Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions go/src/oauth2client/oauth2client.go
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
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


// 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.")
}
}
21 changes: 21 additions & 0 deletions go/src/oauth2client/token.go
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"`
}
86 changes: 86 additions & 0 deletions go/src/oauth2l/main.go
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)
}