-
Notifications
You must be signed in to change notification settings - Fork 86
Add oauth2l-go #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add oauth2l-go #21
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| package oauth2client | ||
|
|
||
| import ( | ||
| "crypto" | ||
| "crypto/rand" | ||
| "crypto/sha256" | ||
| "crypto/x509" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "encoding/pem" | ||
| "fmt" | ||
| "io" | ||
| "io/ioutil" | ||
| "net/http" | ||
| "net/url" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| const ( | ||
| // The URN for getting verification token offline | ||
| oobCallbackUrn = "urn:ietf:wg:oauth:2.0:oob" | ||
| // The URN for token request grant type jwt-bearer | ||
| jwtBearerUrn = "urn:ietf:params:oauth:grant-type:jwt-bearer" | ||
| ) | ||
|
|
||
| // handle 3LO authorize flow. prints the authorization URL on stdout and reads | ||
| // the verification code form stdin. | ||
| func defaultAuthorizeFlowHandler(authorizeUrl string) (string, error) { | ||
| // Print the url on console, let user authorize and paste the token back. | ||
| fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authorizeUrl) | ||
|
|
||
| fmt.Println("Enter verification code: ") | ||
| var code string | ||
| fmt.Scanln(&code) | ||
| return code, nil | ||
| } | ||
|
|
||
| func toString(s interface{}) string { | ||
| return fmt.Sprintf("%v", s) | ||
| } | ||
|
|
||
| // Run the three-legged oauth authorize flow. | ||
| func authorizeFlow(secret map[string]interface{}, scope string, handler func(string) (string, error)) (string, error) { | ||
| // Marshaw a url to be printed on console. In web based oauth flow, the | ||
| // browser should redirect the user to this url | ||
| params := url.Values{ | ||
| "access_type": []string{"offline"}, | ||
| "auth_provider_x509_cert_url": nil, | ||
| "redirect_uri": []string{oobCallbackUrn}, | ||
| "response_type": []string{"code"}, | ||
| "client_id": nil, | ||
| "scope": []string{scope}, | ||
| "project_id": nil, | ||
| } | ||
|
|
||
| for key := range params { | ||
| if val, ok := secret[key]; ok { | ||
| params.Set(key, toString(val)) | ||
| } | ||
| } | ||
|
|
||
| // Call the handler function to handle the authorize url. Return the verify | ||
| // code from handler. | ||
| return handler(toString(secret["auth_uri"]) + "?" + params.Encode()) | ||
| } | ||
|
|
||
| func retrieveAccessToken(url string, params url.Values) (*Token, error) { | ||
| response, err := http.PostForm(url, params) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer response.Body.Close() | ||
| body, err := ioutil.ReadAll(response.Body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| var token *Token | ||
| if err := json.Unmarshal(body, &token); err != nil { | ||
| return nil, err | ||
| } | ||
| return token, nil | ||
| } | ||
|
|
||
| // Run the three-legged oauth verification. Sends a request to auth_uri | ||
| // containing the verification code. | ||
| func verifyFlow(secret map[string]interface{}, scope string, code string) (*Token, error) { | ||
| // Construct a POST request to fetch oauth token using the verificaton code. | ||
| params := url.Values{ | ||
| "client_id": []string{toString(secret["client_id"])}, | ||
| "code": []string{code}, | ||
| "scope": []string{scope}, | ||
| "grant_type": []string{"authorization_code"}, | ||
| "redirect_uri": []string{oobCallbackUrn}, | ||
| } | ||
| if clientSecret, ok := secret["client_secret"]; ok { | ||
| params.Set("client_secret", toString(clientSecret)) | ||
| } | ||
|
|
||
| // Send the POST request and return token. | ||
| return retrieveAccessToken(toString(secret["token_uri"]), params) | ||
| } | ||
|
|
||
| // Helper struct used in sign JWT | ||
| type sha256Opts struct{} | ||
|
|
||
| func (r sha256Opts) HashFunc() crypto.Hash { | ||
| return crypto.SHA256 | ||
| } | ||
|
|
||
| // Base 64 encode a block. The output doesn't contain the trailing double equal | ||
| // signs. | ||
| func base64Encode(b []byte) string { | ||
| return strings.TrimSuffix(base64.URLEncoding.EncodeToString(b), "==") | ||
| } | ||
|
|
||
| // Signer interface to support both rsa and ecdsa sign. | ||
| type pkeyInterface interface { | ||
| Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) ([]byte, error) | ||
| } | ||
|
|
||
| // Convert map to a base64 encoded json. | ||
| func mapToJsonBase64(m map[string]string) (string, error) { | ||
| b, err := json.Marshal(m) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return base64Encode(b), nil | ||
| } | ||
|
|
||
| // Creates a signed JWT token. Used for two-legged oauth token request. | ||
| func createJWT(secret map[string]interface{}, scope string, pkey pkeyInterface) (string, error) { | ||
| // A valid JWT has an "iat" timestamp and an "exp" timestamp. Get the current | ||
| // time to create these timestamps. | ||
| now := int(time.Now().Unix()) | ||
|
|
||
| // Construct the JWT header, which contains the private key id in the service | ||
| // account secret. | ||
| header := map[string]string{ | ||
| "typ": "JWT", | ||
| "alg": "RS256", | ||
| "kid": toString(secret["private_key_id"]), | ||
| } | ||
|
|
||
| // Construct the JWT payload. | ||
| payload := map[string]string{ | ||
| "aud": toString(secret["token_uri"]), | ||
| "scope": scope, | ||
| "iat": strconv.Itoa(now), | ||
| "exp": strconv.Itoa(now + 3600), | ||
| "iss": toString(secret["client_email"]), | ||
| } | ||
|
|
||
| // Convert the header and payload map to json. | ||
| headerB64, err := mapToJsonBase64(header) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| payloadB64, err := mapToJsonBase64(payload) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| // The first two segments of the JWT are signed. The signature is the third | ||
| // segment. | ||
| segments := headerB64 + "." + payloadB64 | ||
|
|
||
| // sign the hash, instead of the actual segments. | ||
| hashed := sha256.Sum256([]byte(segments)) | ||
| signedBytes, err := pkey.Sign(rand.Reader, hashed[:], crypto.SignerOpts(sha256Opts{})) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| // Generate the final JWT as | ||
| // base64(header) + "." + base64(payload) + "." + base64(signature) | ||
| return segments + "." + base64Encode(signedBytes), nil | ||
| } | ||
|
|
||
| // Interface for OAuth2 Client | ||
| type Client interface { | ||
| // Get an OAuth 2 access token for the specified OAuth scopes. | ||
| // scope: A space separated scope codes per OAuth 2.0 spec | ||
| // (https://tools.ietf.org/html/rfc6749). | ||
| // returns: the Token object. It does not contain scope information. | ||
| // GetToken returns a token or an error. | ||
| // GetToken must be safe for concurrent use by multiple goroutines. | ||
| // The returned Token must not be modified. | ||
| GetToken(scope string) (*Token, error) | ||
| } | ||
|
|
||
| type TwoLeggedClient struct { | ||
| secret map[string]interface{} | ||
| } | ||
|
|
||
| type ThreeLeggedClient struct { | ||
| secret map[string]interface{} | ||
| authorizeHandler func(string) (string, error) | ||
| } | ||
|
|
||
| // Run the three-legged oauth flow, including a authorize flow and a verify | ||
| // flow. Returns the token object. | ||
| func (c ThreeLeggedClient) GetToken(scope string) (*Token, error) { | ||
| // In the authorize flow, user will paste a verification code back to console. | ||
| code, err := authorizeFlow(c.secret, scope, c.authorizeHandler) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // The verify flow takes in the verification code from authorize flow, sends a | ||
| // POST request containing the code to fetch oauth token. | ||
| return verifyFlow(c.secret, scope, code) | ||
| } | ||
|
|
||
| // Run the two-legged oauth flow, will create a JWT token and use the JWT token | ||
| // to fetch an oauth token. Returns the token Object | ||
| func (c TwoLeggedClient) GetToken(scope string) (*Token, error) { | ||
| // Read the private key in service account secret. | ||
| pemBytes := []byte(toString(c.secret["private_key"])) | ||
| block, _ := pem.Decode(pemBytes) | ||
| if block == nil { | ||
| return nil, fmt.Errorf("Failed to read private key pem block.") | ||
| } | ||
|
|
||
| // Ignore error, handle the error case below. | ||
| pkcs8key, err := x509.ParsePKCS8PrivateKey(block.Bytes) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Create a pkeyInterface object containing the private key. The | ||
| // pkeyInterface object has a sign function to sign a hash. | ||
| pkey, ok := pkcs8key.(pkeyInterface) | ||
| if !ok { | ||
| return nil, fmt.Errorf("Failed to parse private key.") | ||
| } | ||
|
|
||
| // Get the JWT token | ||
| jwt, err := createJWT(c.secret, scope, pkey) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Construct the POST request to fetch the OAuth token. | ||
| params := url.Values{ | ||
| "assertion": []string{jwt}, | ||
| "grant_type": []string{jwtBearerUrn}, | ||
| } | ||
|
|
||
| // Send the POST request and return token. | ||
| return retrieveAccessToken(toString(c.secret["token_uri"]), params) | ||
| } | ||
|
|
||
| // Create a new OAuth2 Client with given Authorize Handler | ||
| // secretBytes: JSON text that represents either an OAuth client ID or a | ||
| // service account. | ||
| // authorizeHandler: a function that handles three-legged OAuth authorize flow. | ||
| // It should take in an URL, let the user authorize access on that URL, and | ||
| // the verify code. If nil, the client will use defaultAuthorizeFlowHandler. | ||
| func NewClient(secretBytes []byte, authorizeHandler func(string) (string, error)) (Client, error) { | ||
| var secret map[string]interface{} | ||
| if err := json.Unmarshal(secretBytes, &secret); err != nil { | ||
| return nil, err | ||
| } | ||
| if authorizeHandler == nil { | ||
| authorizeHandler = defaultAuthorizeFlowHandler | ||
| } | ||
|
|
||
| // TODO: support "web" client secret by using a local web server. | ||
| // According to the content in the json, decide whether to run three-legged | ||
| // flow (for client secret) or two-legged flow (for service account). | ||
| if installed, ok := secret["installed"]; ok { | ||
| // When the secret contains "installed" field, it is a client secret. We | ||
| // will run a three-legged flow | ||
| installedMap, ok := installed.(map[string]interface{}) | ||
| if !ok { | ||
| return nil, fmt.Errorf("Malformatted secret json, expected map for param \"installed\"") | ||
| } | ||
| return ThreeLeggedClient{installedMap, authorizeHandler}, nil | ||
| } else if tokenType, ok := secret["type"]; ok && "service_account" == tokenType { | ||
| // If the token type is "service_account", we will run the two-legged flow | ||
| return TwoLeggedClient{secret}, nil | ||
| } else { | ||
| return nil, fmt.Errorf("Unsupported token type.") | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package oauth2client | ||
|
|
||
| // Definition for OAuth2 token type. | ||
| // Referenced from https://godoc.org/golang.org/x/oauth2#Token | ||
| type Token struct { | ||
| // AccessToken is the token that authorizes and authenticates | ||
| // the requests. | ||
| AccessToken string `json:"access_token"` | ||
|
|
||
| // TokenType is the type of token. | ||
| // The Type method returns either this or "Bearer", the default. | ||
| TokenType string `json:"token_type,omitempty"` | ||
|
|
||
| // RefreshToken is a token that's used by the application | ||
| // (as opposed to the user) to refresh the access token | ||
| // if it expires. | ||
| RefreshToken string `json:"refresh_token,omitempty"` | ||
|
|
||
| // ExpiresIn is the optional expiration time in seconds. | ||
| ExpiresIn int `json:"expires_in,omitempty"` | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "flag" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "oauth2client" | ||
| "strings" | ||
| ) | ||
|
|
||
| const ( | ||
| // Common prefix for google oauth scope | ||
| scopePrefix = "https://www.googleapis.com/auth/" | ||
| ) | ||
|
|
||
| func help() { | ||
| fmt.Println("Usage: oauth2l --json <secret.json> " + | ||
| "{fetch|header|token} scope1 scope2 ...") | ||
| } | ||
|
|
||
| func fetch(token *oauth2client.Token) { | ||
| fmt.Println(token.AccessToken) | ||
| } | ||
|
|
||
| func header(token *oauth2client.Token) { | ||
| fmt.Printf("Authorization: %s %s\n", token.TokenType, token.AccessToken) | ||
| } | ||
|
|
||
| func token(token *oauth2client.Token) { | ||
| jsonStr, err := json.MarshalIndent(token, "", " ") | ||
| if err != nil { | ||
| panic("Failed to covert token to json.") | ||
| } | ||
| fmt.Println(string(jsonStr)) | ||
| } | ||
|
|
||
| func main() { | ||
| jsonFile := flag.String("json", "", "Path to secret json file.") | ||
| helpFlag := flag.Bool("help", false, "Print help message.") | ||
| flag.BoolVar(helpFlag, "h", false, "") | ||
|
|
||
| flag.Parse() | ||
|
|
||
| if *helpFlag || len(flag.Args()) < 2 { | ||
| help() | ||
| return | ||
| } | ||
|
|
||
| commands := map[string]func(*oauth2client.Token){ | ||
| "fetch": fetch, | ||
| "header": header, | ||
| "token": token, | ||
| } | ||
| secretBytes, err := ioutil.ReadFile(*jsonFile) | ||
| if err != nil { | ||
| fmt.Printf("Failed to read file %s.\n", *jsonFile) | ||
| return | ||
| } | ||
|
|
||
| cmdFunc, ok := commands[flag.Args()[0]] | ||
| if !ok { | ||
| help() | ||
| return | ||
| } | ||
|
|
||
| scopes := flag.Args()[1:] | ||
| // Append Google OAuth scope prefix if not provided. | ||
| for i := 0; i < len(scopes); i++ { | ||
| if !strings.Contains(scopes[i], "//") { | ||
| scopes[i] = scopePrefix + scopes[i] | ||
| } | ||
| } | ||
| client, err := oauth2client.NewClient(secretBytes, nil) | ||
| if err != nil { | ||
| fmt.Printf("Failed to create OAuth2 client: %s\n", err) | ||
| return | ||
| } | ||
| token, err := client.GetToken(strings.Join(scopes, " ")) | ||
| if err != nil { | ||
| fmt.Printf("Error getting token: %s\n", err) | ||
| return | ||
| } | ||
|
|
||
| cmdFunc(token) | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Combine above 3 lines into one. The two local variables are only used once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done