diff --git a/go/src/oauth2client/oauth2client.go b/go/src/oauth2client/oauth2client.go new file mode 100644 index 00000000..c4ee025c --- /dev/null +++ b/go/src/oauth2client/oauth2client.go @@ -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.") + } +} diff --git a/go/src/oauth2client/token.go b/go/src/oauth2client/token.go new file mode 100644 index 00000000..fb03ba4d --- /dev/null +++ b/go/src/oauth2client/token.go @@ -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"` +} diff --git a/go/src/oauth2l/main.go b/go/src/oauth2l/main.go new file mode 100644 index 00000000..9da31ab6 --- /dev/null +++ b/go/src/oauth2l/main.go @@ -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 " + + "{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) +}