diff --git a/third_party/src/code.google.com/p/goauth2/oauth/example/oauthreq.go b/third_party/src/code.google.com/p/goauth2/oauth/example/oauthreq.go new file mode 100755 index 00000000000..0bedc754bcc --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/example/oauthreq.go @@ -0,0 +1,100 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This program makes a call to the specified API, authenticated with OAuth2. +// a list of example APIs can be found at https://code.google.com/oauthplayground/ +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + + "code.google.com/p/goauth2/oauth" +) + +var ( + clientId = flag.String("id", "", "Client ID") + clientSecret = flag.String("secret", "", "Client Secret") + scope = flag.String("scope", "https://www.googleapis.com/auth/userinfo.profile", "OAuth scope") + redirectURL = flag.String("redirect_url", "oob", "Redirect URL") + authURL = flag.String("auth_url", "https://accounts.google.com/o/oauth2/auth", "Authentication URL") + tokenURL = flag.String("token_url", "https://accounts.google.com/o/oauth2/token", "Token URL") + requestURL = flag.String("request_url", "https://www.googleapis.com/oauth2/v1/userinfo", "API request") + code = flag.String("code", "", "Authorization Code") + cachefile = flag.String("cache", "cache.json", "Token cache file") +) + +const usageMsg = ` +To obtain a request token you must specify both -id and -secret. + +To obtain Client ID and Secret, see the "OAuth 2 Credentials" section under +the "API Access" tab on this page: https://code.google.com/apis/console/ + +Once you have completed the OAuth flow, the credentials should be stored inside +the file specified by -cache and you may run without the -id and -secret flags. +` + +func main() { + flag.Parse() + + // Set up a configuration. + config := &oauth.Config{ + ClientId: *clientId, + ClientSecret: *clientSecret, + RedirectURL: *redirectURL, + Scope: *scope, + AuthURL: *authURL, + TokenURL: *tokenURL, + TokenCache: oauth.CacheFile(*cachefile), + } + + // Set up a Transport using the config. + transport := &oauth.Transport{Config: config} + + // Try to pull the token from the cache; if this fails, we need to get one. + token, err := config.TokenCache.Token() + if err != nil { + if *clientId == "" || *clientSecret == "" { + flag.Usage() + fmt.Fprint(os.Stderr, usageMsg) + os.Exit(2) + } + if *code == "" { + // Get an authorization code from the data provider. + // ("Please ask the user if I can access this resource.") + url := config.AuthCodeURL("") + fmt.Println("Visit this URL to get a code, then run again with -code=YOUR_CODE\n") + fmt.Println(url) + return + } + // Exchange the authorization code for an access token. + // ("Here's the code you gave the user, now give me a token!") + token, err = transport.Exchange(*code) + if err != nil { + log.Fatal("Exchange:", err) + } + // (The Exchange method will automatically cache the token.) + fmt.Printf("Token is cached in %v\n", config.TokenCache) + } + + // Make the actual request using the cached token to authenticate. + // ("Here's the token, let me in!") + transport.Token = token + + // Make the request. + r, err := transport.Client().Get(*requestURL) + if err != nil { + log.Fatal("Get:", err) + } + defer r.Body.Close() + + // Write the response to standard output. + io.Copy(os.Stdout, r.Body) + + // Send final carriage return, just to be neat. + fmt.Println() +} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json new file mode 100644 index 00000000000..2ea86f2fc29 --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.client_secrets.json @@ -0,0 +1 @@ +{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"XXXXXXXXXXXX@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXX@developer.gserviceaccount.com","client_id":"XXXXXXXXXXXX.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem new file mode 100644 index 00000000000..8f78b922d69 --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/example.pem @@ -0,0 +1,20 @@ +Bag Attributes + friendlyName: privatekey + localKeyID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +Key Attributes: +-----BEGIN PRIVATE KEY----- +XXXXxyXXXXXXXxxyxxxX9y0XXYXXXXYXXxXyxxXxXxXXXyXXXXx4yx1xy1xyYxxY +1XxYy38YxXxxxyXxyyxx+xxxxyx1Y1xYx7yx2/Y1XyyXYYYxY5YXxX0xY/Y642yX +zYYxYXzXYxY0Y8y9YxyYXxxX40YyXxxXX4XXxx7XxXxxXyXxYYXxXyxX5XY0Yy2X +1YX0XXyy6YXyXx9XxXxyXX9XXYXxXxXXXXXXxYXYY3Y8Yy311XYYY81XyY14Xyyx +xXyx7xxXXXxxxxyyyX4YYYXyYyYXyxX4XYXYyxXYyx9xy23xXYyXyxYxXxx1XXXY +y98yX6yYxyyyX4Xyx1Xy/0yxxYxXxYYx2xx7yYXXXxYXXXxyXyyYYxx5XX2xxyxy +y6Yyyx0XX3YYYyx9YYXXXX7y0yxXXy+90XYz1y2xyx7yXxX+8X0xYxXXYxxyxYYy +YXx8Yy4yX0Xyxxx6yYX92yxy1YYYzyyyyxy55x/yyXXXYYXYXXzXXxYYxyXY8XXX ++y9+yXxX7XxxyYYxxXYxyY623xxXxYX59x5Y6yYyXYY4YxXXYXXXYxXYxXxXXx6x +YXX7XxXX2X0XY7YXyYy1XXxYXxXxYY1xXXxxxyy+07zXYxYxxXyyxxyxXx1XYy5X +5XYzyxYxXXYyX9XX7xX8xXxx+XXYyYXXXX5YY1x8Yxyx54Xy/1XXyyYXY5YxYyxY +XyyxXyX/YxxXXXxXXYXxyxx63xX/xxyYXXyYzx0XY+YxX5xyYyyxxxXXYX/94XXy +Xx63xYxXyXY3/XXxyyXX15XXXyz08XYY5YYXY/YXy/96x68XyyXXxYyXy4xYXx5x +7yxxyxxYxXxyx3y= +-----END PRIVATE KEY----- diff --git a/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/main.go b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/main.go new file mode 100644 index 00000000000..2256e9c621c --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/jwt/example/main.go @@ -0,0 +1,114 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This program makes a read only call to the Google Cloud Storage API, +// authenticated with OAuth2. A list of example APIs can be found at +// https://code.google.com/oauthplayground/ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "code.google.com/p/goauth2/oauth/jwt" +) + +const scope = "https://www.googleapis.com/auth/devstorage.read_only" + +var ( + secretsFile = flag.String("s", "", "JSON encoded secrets for the service account") + pemFile = flag.String("k", "", "private pem key file for the service account") +) + +const usageMsg = ` +You must specify -k and -s. + +To obtain client secrets and pem, see the "OAuth 2 Credentials" section under +the "API Access" tab on this page: https://code.google.com/apis/console/ + +Google Cloud Storage must also be turned on in the API console. +` + +func main() { + flag.Parse() + + if *secretsFile == "" || *pemFile == "" { + flag.Usage() + fmt.Println(usageMsg) + return + } + + // Read the secret file bytes into the config. + secretBytes, err := ioutil.ReadFile(*secretsFile) + if err != nil { + log.Fatal("error reading secerets file:", err) + } + var config struct { + Web struct { + ClientEmail string `json:"client_email"` + ClientID string `json:"client_id"` + TokenURI string `json:"token_uri"` + } + } + err = json.Unmarshal(secretBytes, &config) + if err != nil { + log.Fatal("error unmarshalling secerets:", err) + } + + // Get the project ID from the client ID. + projectID := strings.SplitN(config.Web.ClientID, "-", 2)[0] + + // Read the pem file bytes for the private key. + keyBytes, err := ioutil.ReadFile(*pemFile) + if err != nil { + log.Fatal("error reading private key file:", err) + } + + // Craft the ClaimSet and JWT token. + t := jwt.NewToken(config.Web.ClientEmail, scope, keyBytes) + t.ClaimSet.Aud = config.Web.TokenURI + + // We need to provide a client. + c := &http.Client{} + + // Get the access token. + o, err := t.Assert(c) + if err != nil { + log.Fatal("assertion error:", err) + } + + // Refresh token will be missing, but this access_token will be good + // for one hour. + fmt.Printf("access_token = %v\n", o.AccessToken) + fmt.Printf("refresh_token = %v\n", o.RefreshToken) + fmt.Printf("expires %v\n", o.Expiry) + + // Form the request to list Google Cloud Storage buckets. + req, err := http.NewRequest("GET", "https://storage.googleapis.com/", nil) + if err != nil { + log.Fatal("http.NewRequest:", err) + } + req.Header.Set("Authorization", "OAuth "+o.AccessToken) + req.Header.Set("x-goog-api-version", "2") + req.Header.Set("x-goog-project-id", projectID) + + // Make the request. + r, err := c.Do(req) + if err != nil { + log.Fatal("API request error:", err) + } + defer r.Body.Close() + + // Write the response to standard output. + res, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Fatal("error reading API request results:", err) + } + fmt.Printf("\nRESULT:\n%s\n", res) +} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt.go b/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt.go new file mode 100644 index 00000000000..61bf5ce9368 --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt.go @@ -0,0 +1,511 @@ +// Copyright 2012 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The jwt package provides support for creating credentials for OAuth2 service +// account requests. +// +// For examples of the package usage please see jwt_test.go. +// Example usage (error handling omitted for brevity): +// +// // Craft the ClaimSet and JWT token. +// iss := "XXXXXXXXXXXX@developer.gserviceaccount.com" +// scope := "https://www.googleapis.com/auth/devstorage.read_only" +// t := jwt.NewToken(iss, scope, pemKeyBytes) +// +// // We need to provide a client. +// c := &http.Client{} +// +// // Get the access token. +// o, _ := t.Assert(c) +// +// // Form the request to the service. +// req, _ := http.NewRequest("GET", "https://storage.googleapis.com/", nil) +// req.Header.Set("Authorization", "OAuth "+o.AccessToken) +// req.Header.Set("x-goog-api-version", "2") +// req.Header.Set("x-goog-project-id", "XXXXXXXXXXXX") +// +// // Make the request. +// result, _ := c.Do(req) +// +// For info on OAuth2 service accounts please see the online documentation. +// https://developers.google.com/accounts/docs/OAuth2ServiceAccount +// +package jwt + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "code.google.com/p/goauth2/oauth" +) + +// These are the default/standard values for this to work for Google service accounts. +const ( + stdAlgorithm = "RS256" + stdType = "JWT" + stdAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer" + stdGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" + stdAud = "https://accounts.google.com/o/oauth2/token" +) + +var ( + ErrInvalidKey = errors.New("Invalid Key") +) + +// base64Encode returns and Base64url encoded version of the input string with any +// trailing "=" stripped. +func base64Encode(b []byte) string { + return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") +} + +// base64Decode decodes the Base64url encoded string +func base64Decode(s string) ([]byte, error) { + // add back missing padding + switch len(s) % 4 { + case 2: + s += "==" + case 3: + s += "=" + } + return base64.URLEncoding.DecodeString(s) +} + +// The JWT claim set contains information about the JWT including the +// permissions being requested (scopes), the target of the token, the issuer, +// the time the token was issued, and the lifetime of the token. +// +// Aud is usually https://accounts.google.com/o/oauth2/token +type ClaimSet struct { + Iss string `json:"iss"` // email address of the client_id of the application making the access token request + Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests + Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional). + Prn string `json:"prn,omitempty"` // email for which the application is requesting delegated access (Optional). + Exp int64 `json:"exp"` + Iat int64 `json:"iat"` + Typ string `json:"typ,omitempty"` + Sub string `json:"sub,omitempty"` // Add support for googleapi delegation support + + // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3 + // This array is marshalled using custom code (see (c *ClaimSet) encode()). + PrivateClaims map[string]interface{} `json:"-"` + + exp time.Time + iat time.Time +} + +// setTimes sets iat and exp to time.Now() and iat.Add(time.Hour) respectively. +// +// Note that these times have nothing to do with the expiration time for the +// access_token returned by the server. These have to do with the lifetime of +// the encoded JWT. +// +// A JWT can be re-used for up to one hour after it was encoded. The access +// token that is granted will also be good for one hour so there is little point +// in trying to use the JWT a second time. +func (c *ClaimSet) setTimes(t time.Time) { + c.iat = t + c.exp = c.iat.Add(time.Hour) +} + +var ( + jsonStart = []byte{'{'} + jsonEnd = []byte{'}'} +) + +// encode returns the Base64url encoded form of the Signature. +func (c *ClaimSet) encode() string { + if c.exp.IsZero() || c.iat.IsZero() { + c.setTimes(time.Now()) + } + if c.Aud == "" { + c.Aud = stdAud + } + c.Exp = c.exp.Unix() + c.Iat = c.iat.Unix() + + b, err := json.Marshal(c) + if err != nil { + panic(err) + } + + if len(c.PrivateClaims) == 0 { + return base64Encode(b) + } + + // Marshal private claim set and then append it to b. + prv, err := json.Marshal(c.PrivateClaims) + if err != nil { + panic(fmt.Errorf("Invalid map of private claims %v", c.PrivateClaims)) + } + + // Concatenate public and private claim JSON objects. + if !bytes.HasSuffix(b, jsonEnd) { + panic(fmt.Errorf("Invalid JSON %s", b)) + } + if !bytes.HasPrefix(prv, jsonStart) { + panic(fmt.Errorf("Invalid JSON %s", prv)) + } + b[len(b)-1] = ',' // Replace closing curly brace with a comma. + b = append(b, prv[1:]...) // Append private claims. + + return base64Encode(b) +} + +// Header describes the algorithm and type of token being generated, +// and optionally a KeyID describing additional parameters for the +// signature. +type Header struct { + Algorithm string `json:"alg"` + Type string `json:"typ"` + KeyId string `json:"kid,omitempty"` +} + +func (h *Header) encode() string { + b, err := json.Marshal(h) + if err != nil { + panic(err) + } + return base64Encode(b) +} + +// A JWT is composed of three parts: a header, a claim set, and a signature. +// The well formed and encoded JWT can then be exchanged for an access token. +// +// The Token is not a JWT, but is is encoded to produce a well formed JWT. +// +// When obtaining a key from the Google API console it will be downloaded in a +// PKCS12 encoding. To use this key you will need to convert it to a PEM file. +// This can be achieved with openssl. +// +// $ openssl pkcs12 -in -nocerts -passin pass:notasecret -nodes -out +// +// The contents of this file can then be used as the Key. +type Token struct { + ClaimSet *ClaimSet // claim set used to construct the JWT + Header *Header // header used to construct the JWT + Key []byte // PEM printable encoding of the private key + pKey *rsa.PrivateKey + + header string + claim string + sig string + + useExternalSigner bool + signer Signer +} + +// NewToken returns a filled in *Token based on the standard header, +// and sets the Iat and Exp times based on when the call to Assert is +// made. +func NewToken(iss, scope string, key []byte) *Token { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + Aud: stdAud, + } + h := &Header{ + Algorithm: stdAlgorithm, + Type: stdType, + } + t := &Token{ + ClaimSet: c, + Header: h, + Key: key, + } + return t +} + +// Signer is an interface that given a JWT token, returns the header & +// claim (serialized and urlEncoded to a byte slice), along with the +// signature and an error (if any occured). It could modify any data +// to sign (typically the KeyID). +// +// Example usage where a SHA256 hash of the original url-encoded token +// with an added KeyID and secret data is used as a signature: +// +// var privateData = "secret data added to hash, indexed by KeyID" +// +// type SigningService struct{} +// +// func (ss *SigningService) Sign(in *jwt.Token) (newTokenData, sig []byte, err error) { +// in.Header.KeyID = "signing service" +// newTokenData = in.EncodeWithoutSignature() +// dataToSign := fmt.Sprintf("%s.%s", newTokenData, privateData) +// h := sha256.New() +// _, err := h.Write([]byte(dataToSign)) +// sig = h.Sum(nil) +// return +// } +type Signer interface { + Sign(in *Token) (tokenData, signature []byte, err error) +} + +// NewSignerToken returns a *Token, using an external signer function +func NewSignerToken(iss, scope string, signer Signer) *Token { + t := NewToken(iss, scope, nil) + t.useExternalSigner = true + t.signer = signer + return t +} + +// Expired returns a boolean value letting us know if the token has expired. +func (t *Token) Expired() bool { + return t.ClaimSet.exp.Before(time.Now()) +} + +// Encode constructs and signs a Token returning a JWT ready to use for +// requesting an access token. +func (t *Token) Encode() (string, error) { + var tok string + t.header = t.Header.encode() + t.claim = t.ClaimSet.encode() + err := t.sign() + if err != nil { + return tok, err + } + tok = fmt.Sprintf("%s.%s.%s", t.header, t.claim, t.sig) + return tok, nil +} + +// EncodeWithoutSignature returns the url-encoded value of the Token +// before signing has occured (typically for use by external signers). +func (t *Token) EncodeWithoutSignature() string { + t.header = t.Header.encode() + t.claim = t.ClaimSet.encode() + return fmt.Sprintf("%s.%s", t.header, t.claim) +} + +// sign computes the signature for a Token. The details for this can be found +// in the OAuth2 Service Account documentation. +// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature +func (t *Token) sign() error { + if t.useExternalSigner { + fulldata, sig, err := t.signer.Sign(t) + if err != nil { + return err + } + split := strings.Split(string(fulldata), ".") + if len(split) != 2 { + return errors.New("no token returned") + } + t.header = split[0] + t.claim = split[1] + t.sig = base64Encode(sig) + return err + } + ss := fmt.Sprintf("%s.%s", t.header, t.claim) + if t.pKey == nil { + err := t.parsePrivateKey() + if err != nil { + return err + } + } + h := sha256.New() + h.Write([]byte(ss)) + b, err := rsa.SignPKCS1v15(rand.Reader, t.pKey, crypto.SHA256, h.Sum(nil)) + t.sig = base64Encode(b) + return err +} + +// parsePrivateKey converts the Token's Key ([]byte) into a parsed +// rsa.PrivateKey. If the key is not well formed this method will return an +// ErrInvalidKey error. +func (t *Token) parsePrivateKey() error { + block, _ := pem.Decode(t.Key) + if block == nil { + return ErrInvalidKey + } + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return err + } + } + var ok bool + t.pKey, ok = parsedKey.(*rsa.PrivateKey) + if !ok { + return ErrInvalidKey + } + return nil +} + +// Assert obtains an *oauth.Token from the remote server by encoding and sending +// a JWT. The access_token will expire in one hour (3600 seconds) and cannot be +// refreshed (no refresh_token is returned with the response). Once this token +// expires call this method again to get a fresh one. +func (t *Token) Assert(c *http.Client) (*oauth.Token, error) { + var o *oauth.Token + t.ClaimSet.setTimes(time.Now()) + u, v, err := t.buildRequest() + if err != nil { + return o, err + } + resp, err := c.PostForm(u, v) + if err != nil { + return o, err + } + o, err = handleResponse(resp) + return o, err +} + +// buildRequest sets up the URL values and the proper URL string for making our +// access_token request. +func (t *Token) buildRequest() (string, url.Values, error) { + v := url.Values{} + j, err := t.Encode() + if err != nil { + return t.ClaimSet.Aud, v, err + } + v.Set("grant_type", stdGrantType) + v.Set("assertion", j) + return t.ClaimSet.Aud, v, nil +} + +// Used for decoding the response body. +type respBody struct { + IdToken string `json:"id_token"` + Access string `json:"access_token"` + Type string `json:"token_type"` + ExpiresIn time.Duration `json:"expires_in"` +} + +// handleResponse returns a filled in *oauth.Token given the *http.Response from +// a *http.Request created by buildRequest. +func handleResponse(r *http.Response) (*oauth.Token, error) { + o := &oauth.Token{} + defer r.Body.Close() + if r.StatusCode != 200 { + return o, errors.New("invalid response: " + r.Status) + } + b := &respBody{} + err := json.NewDecoder(r.Body).Decode(b) + if err != nil { + return o, err + } + o.AccessToken = b.Access + if b.IdToken != "" { + // decode returned id token to get expiry + o.AccessToken = b.IdToken + s := strings.Split(b.IdToken, ".") + if len(s) < 2 { + return nil, errors.New("invalid token received") + } + d, err := base64Decode(s[1]) + if err != nil { + return o, err + } + c := &ClaimSet{} + err = json.NewDecoder(bytes.NewBuffer(d)).Decode(c) + if err != nil { + return o, err + } + o.Expiry = time.Unix(c.Exp, 0) + return o, nil + } + o.Expiry = time.Now().Add(b.ExpiresIn * time.Second) + return o, nil +} + +// Transport implements http.RoundTripper. When configured with a valid +// JWT and OAuth tokens it can be used to make authenticated HTTP requests. +// +// t := &jwt.Transport{jwtToken, oauthToken} +// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") +// +// It will automatically refresh the OAuth token if it can, updating in place. +type Transport struct { + JWTToken *Token + OAuthToken *oauth.Token + + // Transport is the HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + Transport http.RoundTripper +} + +// Creates a new authenticated transport. +func NewTransport(token *Token) (*Transport, error) { + oa, err := token.Assert(new(http.Client)) + if err != nil { + return nil, err + } + return &Transport{ + JWTToken: token, + OAuthToken: oa, + }, nil +} + +// Client returns an *http.Client that makes OAuth-authenticated requests. +func (t *Transport) Client() *http.Client { + return &http.Client{Transport: t} +} + +// Fetches the internal transport. +func (t *Transport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// RoundTrip executes a single HTTP transaction using the Transport's +// OAuthToken as authorization headers. +// +// This method will attempt to renew the token if it has expired and may return +// an error related to that token renewal before attempting the client request. +// If the token cannot be renewed a non-nil os.Error value will be returned. +// If the token is invalid callers should expect HTTP-level errors, +// as indicated by the Response's StatusCode. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + // Sanity check the two tokens + if t.JWTToken == nil { + return nil, fmt.Errorf("no JWT token supplied") + } + if t.OAuthToken == nil { + return nil, fmt.Errorf("no OAuth token supplied") + } + // Refresh the OAuth token if it has expired + if t.OAuthToken.Expired() { + if oa, err := t.JWTToken.Assert(new(http.Client)); err != nil { + return nil, err + } else { + t.OAuthToken = oa + } + } + // To set the Authorization header, we must make a copy of the Request + // so that we don't modify the Request we were given. + // This is required by the specification of http.RoundTripper. + req = cloneRequest(req) + req.Header.Set("Authorization", "Bearer "+t.OAuthToken.AccessToken) + + // Make the HTTP request. + return t.transport().RoundTrip(req) +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go b/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go new file mode 100644 index 00000000000..622843e1686 --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/jwt/jwt_test.go @@ -0,0 +1,486 @@ +// Copyright 2012 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// For package documentation please see jwt.go. +// +package jwt + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "io/ioutil" + "net/http" + "testing" + "time" +) + +const ( + stdHeaderStr = `{"alg":"RS256","typ":"JWT"}` + iss = "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com" + scope = "https://www.googleapis.com/auth/prediction" + exp = 1328554385 + iat = 1328550785 // exp + 1 hour +) + +// Base64url encoded Header +const headerEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" + +// Base64url encoded ClaimSet +const claimSetEnc = "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ" + +// Base64url encoded Signature +const sigEnc = "olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g" + +// Base64url encoded Token +const tokEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g" + +// Private key for testing +const privateKeyPem = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj +7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/ +xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs +SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18 +pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk +SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk +nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq +HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y +nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9 +IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2 +YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU +Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ +vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP +B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl +aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2 +eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI +aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk +klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ +CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu +UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg +soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28 +bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH +504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL +YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx +BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg== +-----END RSA PRIVATE KEY-----` + +// Public key to go with the private key for testing +const publicKeyPem = `-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE-----` + +var ( + privateKeyPemBytes = []byte(privateKeyPem) + publicKeyPemBytes = []byte(publicKeyPem) + stdHeader = &Header{Algorithm: stdAlgorithm, Type: stdType} +) + +// Testing the urlEncode function. +func TestUrlEncode(t *testing.T) { + enc := base64Encode([]byte(stdHeaderStr)) + b := []byte(enc) + if b[len(b)-1] == 61 { + t.Error("TestUrlEncode: last chat == \"=\"") + } + if enc != headerEnc { + t.Error("TestUrlEncode: enc != headerEnc") + t.Errorf(" enc = %s", enc) + t.Errorf(" headerEnc = %s", headerEnc) + } +} + +// Test that the times are set properly. +func TestClaimSetSetTimes(t *testing.T) { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + } + iat := time.Unix(iat, 0) + c.setTimes(iat) + if c.exp.Unix() != exp { + t.Error("TestClaimSetSetTimes: c.exp != exp") + t.Errorf(" c.Exp = %d", c.exp.Unix()) + t.Errorf(" exp = %d", exp) + } +} + +// Given a well formed ClaimSet, test for proper encoding. +func TestClaimSetEncode(t *testing.T) { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + exp: time.Unix(exp, 0), + iat: time.Unix(iat, 0), + } + enc := c.encode() + re, err := base64Decode(enc) + if err != nil { + t.Fatalf("error decoding encoded claim set: %v", err) + } + + wa, err := base64Decode(claimSetEnc) + if err != nil { + t.Fatalf("error decoding encoded expected claim set: %v", err) + } + + if enc != claimSetEnc { + t.Error("TestClaimSetEncode: enc != claimSetEnc") + t.Errorf(" enc = %s", string(re)) + t.Errorf(" claimSetEnc = %s", string(wa)) + } +} + +// Test that claim sets with private claim names are encoded correctly. +func TestClaimSetWithPrivateNameEncode(t *testing.T) { + iatT := time.Unix(iat, 0) + expT := time.Unix(exp, 0) + + i, err := json.Marshal(iatT.Unix()) + if err != nil { + t.Fatalf("error marshaling iatT value of %v: %v", iatT.Unix(), err) + } + iatStr := string(i) + e, err := json.Marshal(expT.Unix()) + if err != nil { + t.Fatalf("error marshaling expT value of %v: %v", expT.Unix(), err) + } + + expStr := string(e) + + testCases := []struct { + desc string + input map[string]interface{} + want string + }{ + // Test a simple int field. + { + "single simple field", + map[string]interface{}{"amount": 22}, + `{` + + `"iss":"` + iss + `",` + + `"scope":"` + scope + `",` + + `"aud":"` + stdAud + `",` + + `"exp":` + expStr + `,` + + `"iat":` + iatStr + `,` + + `"amount":22` + + `}`, + }, + { + "multiple simple fields", + map[string]interface{}{"tracking_code": "axZf", "amount": 22}, + `{` + + `"iss":"` + iss + `",` + + `"scope":"` + scope + `",` + + `"aud":"` + stdAud + `",` + + `"exp":` + expStr + `,` + + `"iat":` + iatStr + `,` + + `"amount":22,` + + `"tracking_code":"axZf"` + + `}`, + }, + { + "nested struct fields", + map[string]interface{}{ + "tracking_code": "axZf", + "purchase": struct { + Description string `json:"desc"` + Quantity int32 `json:"q"` + Time int64 `json:"t"` + }{ + "toaster", + 5, + iat, + }, + }, + `{` + + `"iss":"` + iss + `",` + + `"scope":"` + scope + `",` + + `"aud":"` + stdAud + `",` + + `"exp":` + expStr + `,` + + `"iat":` + iatStr + `,` + + `"purchase":{"desc":"toaster","q":5,"t":` + iatStr + `},` + + `"tracking_code":"axZf"` + + `}`, + }, + } + + for _, testCase := range testCases { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + Aud: stdAud, + iat: iatT, + exp: expT, + PrivateClaims: testCase.input, + } + cJSON, err := base64Decode(c.encode()) + if err != nil { + t.Fatalf("error decoding claim set: %v", err) + } + if string(cJSON) != testCase.want { + t.Errorf("TestClaimSetWithPrivateNameEncode: enc != want in case %s", testCase.desc) + t.Errorf(" enc = %s", cJSON) + t.Errorf(" want = %s", testCase.want) + } + } +} + +// Test the NewToken constructor. +func TestNewToken(t *testing.T) { + tok := NewToken(iss, scope, privateKeyPemBytes) + if tok.ClaimSet.Iss != iss { + t.Error("TestNewToken: tok.ClaimSet.Iss != iss") + t.Errorf(" tok.ClaimSet.Iss = %s", tok.ClaimSet.Iss) + t.Errorf(" iss = %s", iss) + } + if tok.ClaimSet.Scope != scope { + t.Error("TestNewToken: tok.ClaimSet.Scope != scope") + t.Errorf(" tok.ClaimSet.Scope = %s", tok.ClaimSet.Scope) + t.Errorf(" scope = %s", scope) + } + if tok.ClaimSet.Aud != stdAud { + t.Error("TestNewToken: tok.ClaimSet.Aud != stdAud") + t.Errorf(" tok.ClaimSet.Aud = %s", tok.ClaimSet.Aud) + t.Errorf(" stdAud = %s", stdAud) + } + if !bytes.Equal(tok.Key, privateKeyPemBytes) { + t.Error("TestNewToken: tok.Key != privateKeyPemBytes") + t.Errorf(" tok.Key = %s", tok.Key) + t.Errorf(" privateKeyPemBytes = %s", privateKeyPemBytes) + } +} + +// Make sure the private key parsing functions work. +func TestParsePrivateKey(t *testing.T) { + tok := &Token{ + Key: privateKeyPemBytes, + } + err := tok.parsePrivateKey() + if err != nil { + t.Errorf("TestParsePrivateKey:tok.parsePrivateKey: %v", err) + } +} + +// Test that the token signature generated matches the golden standard. +func TestTokenSign(t *testing.T) { + tok := &Token{ + Key: privateKeyPemBytes, + claim: claimSetEnc, + header: headerEnc, + } + err := tok.parsePrivateKey() + if err != nil { + t.Errorf("TestTokenSign:tok.parsePrivateKey: %v", err) + } + err = tok.sign() + if err != nil { + t.Errorf("TestTokenSign:tok.sign: %v", err) + } + if tok.sig != sigEnc { + t.Error("TestTokenSign: tok.sig != sigEnc") + t.Errorf(" tok.sig = %s", tok.sig) + t.Errorf(" sigEnc = %s", sigEnc) + } +} + +// Test that the token expiration function is working. +func TestTokenExpired(t *testing.T) { + c := &ClaimSet{} + tok := &Token{ + ClaimSet: c, + } + now := time.Now() + c.setTimes(now) + if tok.Expired() != false { + t.Error("TestTokenExpired: tok.Expired != false") + } + // Set the times as if they were set 2 hours ago. + c.setTimes(now.Add(-2 * time.Hour)) + if tok.Expired() != true { + t.Error("TestTokenExpired: tok.Expired != true") + } +} + +// Given a well formed Token, test for proper encoding. +func TestTokenEncode(t *testing.T) { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + exp: time.Unix(exp, 0), + iat: time.Unix(iat, 0), + } + tok := &Token{ + ClaimSet: c, + Header: stdHeader, + Key: privateKeyPemBytes, + } + enc, err := tok.Encode() + if err != nil { + t.Errorf("TestTokenEncode:tok.Assertion: %v", err) + } + if enc != tokEnc { + t.Error("TestTokenEncode: enc != tokEnc") + t.Errorf(" enc = %s", enc) + t.Errorf(" tokEnc = %s", tokEnc) + } +} + +// Given a well formed Token we should get back a well formed request. +func TestBuildRequest(t *testing.T) { + c := &ClaimSet{ + Iss: iss, + Scope: scope, + exp: time.Unix(exp, 0), + iat: time.Unix(iat, 0), + } + tok := &Token{ + ClaimSet: c, + Header: stdHeader, + Key: privateKeyPemBytes, + } + u, v, err := tok.buildRequest() + if err != nil { + t.Errorf("TestBuildRequest:BuildRequest: %v", err) + } + if u != c.Aud { + t.Error("TestBuildRequest: u != c.Aud") + t.Errorf(" u = %s", u) + t.Errorf(" c.Aud = %s", c.Aud) + } + if v.Get("grant_type") != stdGrantType { + t.Error("TestBuildRequest: grant_type != stdGrantType") + t.Errorf(" grant_type = %s", v.Get("grant_type")) + t.Errorf(" stdGrantType = %s", stdGrantType) + } + if v.Get("assertion") != tokEnc { + t.Error("TestBuildRequest: assertion != tokEnc") + t.Errorf(" assertion = %s", v.Get("assertion")) + t.Errorf(" tokEnc = %s", tokEnc) + } +} + +// Given a well formed access request response we should get back a oauth.Token. +func TestHandleResponse(t *testing.T) { + rb := &respBody{ + Access: "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M", + Type: "Bearer", + ExpiresIn: 3600, + } + b, err := json.Marshal(rb) + if err != nil { + t.Errorf("TestHandleResponse:json.Marshal: %v", err) + } + r := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + o, err := handleResponse(r) + if err != nil { + t.Errorf("TestHandleResponse:handleResponse: %v", err) + } + if o.AccessToken != rb.Access { + t.Error("TestHandleResponse: o.AccessToken != rb.Access") + t.Errorf(" o.AccessToken = %s", o.AccessToken) + t.Errorf(" rb.Access = %s", rb.Access) + } + if o.Expired() { + t.Error("TestHandleResponse: o.Expired == true") + } +} + +// passthrough signature for test +type FakeSigner struct{} + +func (f FakeSigner) Sign(tok *Token) ([]byte, []byte, error) { + block, _ := pem.Decode(privateKeyPemBytes) + pKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + ss := headerEnc + "." + claimSetEnc + h := sha256.New() + h.Write([]byte(ss)) + b, _ := rsa.SignPKCS1v15(rand.Reader, pKey, crypto.SHA256, h.Sum(nil)) + return []byte(ss), b, nil +} + +// Given an external signer, get back a valid and signed JWT +func TestExternalSigner(t *testing.T) { + tok := NewSignerToken(iss, scope, FakeSigner{}) + enc, _ := tok.Encode() + if enc != tokEnc { + t.Errorf("TestExternalSigner: enc != tokEnc") + t.Errorf(" enc = %s", enc) + t.Errorf(" tokEnc = %s", tokEnc) + } +} + +func TestHandleResponseWithNewExpiry(t *testing.T) { + rb := &respBody{ + IdToken: tokEnc, + } + b, err := json.Marshal(rb) + if err != nil { + t.Errorf("TestHandleResponse:json.Marshal: %v", err) + } + r := &http.Response{ + Status: "200 OK", + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + o, err := handleResponse(r) + if err != nil { + t.Errorf("TestHandleResponse:handleResponse: %v", err) + } + if o.Expiry != time.Unix(exp, 0) { + t.Error("TestHandleResponse: o.Expiry != exp") + t.Errorf(" o.Expiry = %s", o.Expiry) + t.Errorf(" exp = %s", time.Unix(exp, 0)) + } +} + +// Placeholder for future Assert tests. +func TestAssert(t *testing.T) { + // Since this method makes a call to BuildRequest, an htttp.Client, and + // finally HandleResponse there is not much more to test. This is here + // as a placeholder if that changes. +} + +// Benchmark for the end-to-end encoding of a well formed token. +func BenchmarkTokenEncode(b *testing.B) { + b.StopTimer() + c := &ClaimSet{ + Iss: iss, + Scope: scope, + exp: time.Unix(exp, 0), + iat: time.Unix(iat, 0), + } + tok := &Token{ + ClaimSet: c, + Key: privateKeyPemBytes, + } + b.StartTimer() + for i := 0; i < b.N; i++ { + tok.Encode() + } +} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/oauth.go b/third_party/src/code.google.com/p/goauth2/oauth/oauth.go new file mode 100644 index 00000000000..eb3ebe40262 --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/oauth.go @@ -0,0 +1,386 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The oauth package provides support for making +// OAuth2-authenticated HTTP requests. +// +// Example usage: +// +// // Specify your configuration. (typically as a global variable) +// var config = &oauth.Config{ +// ClientId: YOUR_CLIENT_ID, +// ClientSecret: YOUR_CLIENT_SECRET, +// Scope: "https://www.googleapis.com/auth/buzz", +// AuthURL: "https://accounts.google.com/o/oauth2/auth", +// TokenURL: "https://accounts.google.com/o/oauth2/token", +// RedirectURL: "http://you.example.org/handler", +// } +// +// // A landing page redirects to the OAuth provider to get the auth code. +// func landing(w http.ResponseWriter, r *http.Request) { +// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound) +// } +// +// // The user will be redirected back to this handler, that takes the +// // "code" query parameter and Exchanges it for an access token. +// func handler(w http.ResponseWriter, r *http.Request) { +// t := &oauth.Transport{Config: config} +// t.Exchange(r.FormValue("code")) +// // The Transport now has a valid Token. Create an *http.Client +// // with which we can make authenticated API requests. +// c := t.Client() +// c.Post(...) +// // ... +// // btw, r.FormValue("state") == "foo" +// } +// +package oauth + +import ( + "encoding/json" + "io/ioutil" + "mime" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type OAuthError struct { + prefix string + msg string +} + +func (oe OAuthError) Error() string { + return "OAuthError: " + oe.prefix + ": " + oe.msg +} + +// Cache specifies the methods that implement a Token cache. +type Cache interface { + Token() (*Token, error) + PutToken(*Token) error +} + +// CacheFile implements Cache. Its value is the name of the file in which +// the Token is stored in JSON format. +type CacheFile string + +func (f CacheFile) Token() (*Token, error) { + file, err := os.Open(string(f)) + if err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + defer file.Close() + tok := &Token{} + if err := json.NewDecoder(file).Decode(tok); err != nil { + return nil, OAuthError{"CacheFile.Token", err.Error()} + } + return tok, nil +} + +func (f CacheFile) PutToken(tok *Token) error { + file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := json.NewEncoder(file).Encode(tok); err != nil { + file.Close() + return OAuthError{"CacheFile.PutToken", err.Error()} + } + if err := file.Close(); err != nil { + return OAuthError{"CacheFile.PutToken", err.Error()} + } + return nil +} + +// Config is the configuration of an OAuth consumer. +type Config struct { + // ClientId is the OAuth client identifier used when communicating with + // the configured OAuth provider. + ClientId string + + // ClientSecret is the OAuth client secret used when communicating with + // the configured OAuth provider. + ClientSecret string + + // Scope identifies the level of access being requested. Multiple scope + // values should be provided as a space-delimited string. + Scope string + + // AuthURL is the URL the user will be directed to in order to grant + // access. + AuthURL string + + // TokenURL is the URL used to retrieve OAuth tokens. + TokenURL string + + // RedirectURL is the URL to which the user will be returned after + // granting (or denying) access. + RedirectURL string + + // TokenCache allows tokens to be cached for subsequent requests. + TokenCache Cache + + AccessType string // Optional, "online" (default) or "offline", no refresh token if "online" + + // ApprovalPrompt indicates whether the user should be + // re-prompted for consent. If set to "auto" (default) the + // user will be prompted only if they haven't previously + // granted consent and the code can only be exchanged for an + // access token. + // If set to "force" the user will always be prompted, and the + // code can be exchanged for a refresh token. + ApprovalPrompt string +} + +// Token contains an end-user's tokens. +// This is the data you must store to persist authentication. +type Token struct { + AccessToken string + RefreshToken string + Expiry time.Time // If zero the token has no (known) expiry time. + Extra map[string]string // May be nil. +} + +func (t *Token) Expired() bool { + if t.Expiry.IsZero() { + return false + } + return t.Expiry.Before(time.Now()) +} + +// Transport implements http.RoundTripper. When configured with a valid +// Config and Token it can be used to make authenticated HTTP requests. +// +// t := &oauth.Transport{config} +// t.Exchange(code) +// // t now contains a valid Token +// r, _, err := t.Client().Get("http://example.org/url/requiring/auth") +// +// It will automatically refresh the Token if it can, +// updating the supplied Token in place. +type Transport struct { + *Config + *Token + + // Transport is the HTTP transport to use when making requests. + // It will default to http.DefaultTransport if nil. + // (It should never be an oauth.Transport.) + Transport http.RoundTripper +} + +// Client returns an *http.Client that makes OAuth-authenticated requests. +func (t *Transport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *Transport) transport() http.RoundTripper { + if t.Transport != nil { + return t.Transport + } + return http.DefaultTransport +} + +// AuthCodeURL returns a URL that the end-user should be redirected to, +// so that they may obtain an authorization code. +func (c *Config) AuthCodeURL(state string) string { + url_, err := url.Parse(c.AuthURL) + if err != nil { + panic("AuthURL malformed: " + err.Error()) + } + q := url.Values{ + "response_type": {"code"}, + "client_id": {c.ClientId}, + "redirect_uri": {c.RedirectURL}, + "scope": {c.Scope}, + "state": {state}, + "access_type": {c.AccessType}, + "approval_prompt": {c.ApprovalPrompt}, + }.Encode() + if url_.RawQuery == "" { + url_.RawQuery = q + } else { + url_.RawQuery += "&" + q + } + return url_.String() +} + +// Exchange takes a code and gets access Token from the remote server. +func (t *Transport) Exchange(code string) (*Token, error) { + if t.Config == nil { + return nil, OAuthError{"Exchange", "no Config supplied"} + } + + // If the transport or the cache already has a token, it is + // passed to `updateToken` to preserve existing refresh token. + tok := t.Token + if tok == nil && t.TokenCache != nil { + tok, _ = t.TokenCache.Token() + } + if tok == nil { + tok = new(Token) + } + err := t.updateToken(tok, url.Values{ + "grant_type": {"authorization_code"}, + "redirect_uri": {t.RedirectURL}, + "scope": {t.Scope}, + "code": {code}, + }) + if err != nil { + return nil, err + } + t.Token = tok + if t.TokenCache != nil { + return tok, t.TokenCache.PutToken(tok) + } + return tok, nil +} + +// RoundTrip executes a single HTTP transaction using the Transport's +// Token as authorization headers. +// +// This method will attempt to renew the Token if it has expired and may return +// an error related to that Token renewal before attempting the client request. +// If the Token cannot be renewed a non-nil os.Error value will be returned. +// If the Token is invalid callers should expect HTTP-level errors, +// as indicated by the Response's StatusCode. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.Token == nil { + if t.Config == nil { + return nil, OAuthError{"RoundTrip", "no Config supplied"} + } + if t.TokenCache == nil { + return nil, OAuthError{"RoundTrip", "no Token supplied"} + } + var err error + t.Token, err = t.TokenCache.Token() + if err != nil { + return nil, err + } + } + + // Refresh the Token if it has expired. + if t.Expired() { + if err := t.Refresh(); err != nil { + return nil, err + } + } + + // To set the Authorization header, we must make a copy of the Request + // so that we don't modify the Request we were given. + // This is required by the specification of http.RoundTripper. + req = cloneRequest(req) + req.Header.Set("Authorization", "Bearer "+t.AccessToken) + + // Make the HTTP request. + return t.transport().RoundTrip(req) +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + // deep copy of the Header + r2.Header = make(http.Header) + for k, s := range r.Header { + r2.Header[k] = s + } + return r2 +} + +// Refresh renews the Transport's AccessToken using its RefreshToken. +func (t *Transport) Refresh() error { + if t.Token == nil { + return OAuthError{"Refresh", "no existing Token"} + } + if t.RefreshToken == "" { + return OAuthError{"Refresh", "Token expired; no Refresh Token"} + } + if t.Config == nil { + return OAuthError{"Refresh", "no Config supplied"} + } + + err := t.updateToken(t.Token, url.Values{ + "grant_type": {"refresh_token"}, + "refresh_token": {t.RefreshToken}, + }) + if err != nil { + return err + } + if t.TokenCache != nil { + return t.TokenCache.PutToken(t.Token) + } + return nil +} + +func (t *Transport) updateToken(tok *Token, v url.Values) error { + v.Set("client_id", t.ClientId) + v.Set("client_secret", t.ClientSecret) + client := &http.Client{Transport: t.transport()} + req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(t.ClientId, t.ClientSecret) + r, err := client.Do(req) + if err != nil { + return err + } + defer r.Body.Close() + if r.StatusCode != 200 { + return OAuthError{"updateToken", r.Status} + } + var b struct { + Access string `json:"access_token"` + Refresh string `json:"refresh_token"` + ExpiresIn time.Duration `json:"expires_in"` + Id string `json:"id_token"` + } + + content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + switch content { + case "application/x-www-form-urlencoded", "text/plain": + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + vals, err := url.ParseQuery(string(body)) + if err != nil { + return err + } + + b.Access = vals.Get("access_token") + b.Refresh = vals.Get("refresh_token") + b.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s") + b.Id = vals.Get("id_token") + default: + if err = json.NewDecoder(r.Body).Decode(&b); err != nil { + return err + } + // The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above, + // so compensate here. + b.ExpiresIn *= time.Second + } + tok.AccessToken = b.Access + // Don't overwrite `RefreshToken` with an empty value + if len(b.Refresh) > 0 { + tok.RefreshToken = b.Refresh + } + if b.ExpiresIn == 0 { + tok.Expiry = time.Time{} + } else { + tok.Expiry = time.Now().Add(b.ExpiresIn) + } + if b.Id != "" { + if tok.Extra == nil { + tok.Extra = make(map[string]string) + } + tok.Extra["id_token"] = b.Id + } + return nil +} diff --git a/third_party/src/code.google.com/p/goauth2/oauth/oauth_test.go b/third_party/src/code.google.com/p/goauth2/oauth/oauth_test.go new file mode 100644 index 00000000000..370d6a5f13b --- /dev/null +++ b/third_party/src/code.google.com/p/goauth2/oauth/oauth_test.go @@ -0,0 +1,189 @@ +// Copyright 2011 The goauth2 Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package oauth + +import ( + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "runtime" + "testing" + "time" +) + +var requests = []struct { + path, query, auth string // request + contenttype, body string // response +}{ + { + path: "/token", + query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d&client_secret=s3cr3t", + contenttype: "application/json", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + body: ` + { + "access_token":"token1", + "refresh_token":"refreshtoken1", + "id_token":"idtoken1", + "expires_in":3600 + } + `, + }, + {path: "/secure", auth: "Bearer token1", body: "first payload"}, + { + path: "/token", + query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d&client_secret=s3cr3t", + contenttype: "application/json", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + body: ` + { + "access_token":"token2", + "refresh_token":"refreshtoken2", + "id_token":"idtoken2", + "expires_in":3600 + } + `, + }, + {path: "/secure", auth: "Bearer token2", body: "second payload"}, + { + path: "/token", + query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d&client_secret=s3cr3t", + contenttype: "application/x-www-form-urlencoded", + body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600", + auth: "Basic Y2wxM250MWQ6czNjcjN0", + }, + {path: "/secure", auth: "Bearer token3", body: "third payload"}, +} + +func TestOAuth(t *testing.T) { + // Set up test server. + n := 0 + handler := func(w http.ResponseWriter, r *http.Request) { + if n >= len(requests) { + t.Errorf("too many requests: %d", n) + return + } + req := requests[n] + n++ + + // Check request. + if g, w := r.URL.Path, req.path; g != w { + t.Errorf("request[%d] got path %s, want %s", n, g, w) + } + want, _ := url.ParseQuery(req.query) + for k := range want { + if g, w := r.FormValue(k), want.Get(k); g != w { + t.Errorf("query[%s] = %s, want %s", k, g, w) + } + } + if g, w := r.Header.Get("Authorization"), req.auth; w != "" && g != w { + t.Errorf("Authorization: %v, want %v", g, w) + } + + // Send response. + w.Header().Set("Content-Type", req.contenttype) + io.WriteString(w, req.body) + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + config := &Config{ + ClientId: "cl13nt1d", + ClientSecret: "s3cr3t", + Scope: "https://example.net/scope", + AuthURL: server.URL + "/auth", + TokenURL: server.URL + "/token", + } + + // TODO(adg): test AuthCodeURL + + transport := &Transport{Config: config} + _, err := transport.Exchange("c0d3") + if err != nil { + t.Fatalf("Exchange: %v", err) + } + checkToken(t, transport.Token, "token1", "refreshtoken1", "idtoken1") + + c := transport.Client() + resp, err := c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "first payload") + + // test automatic refresh + transport.Expiry = time.Now().Add(-time.Hour) + resp, err = c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "second payload") + checkToken(t, transport.Token, "token2", "refreshtoken2", "idtoken2") + + // refresh one more time, but get URL-encoded token instead of JSON + transport.Expiry = time.Now().Add(-time.Hour) + resp, err = c.Get(server.URL + "/secure") + if err != nil { + t.Fatalf("Get: %v", err) + } + checkBody(t, resp, "third payload") + checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3") +} + +func checkToken(t *testing.T, tok *Token, access, refresh, id string) { + if g, w := tok.AccessToken, access; g != w { + t.Errorf("AccessToken = %q, want %q", g, w) + } + if g, w := tok.RefreshToken, refresh; g != w { + t.Errorf("RefreshToken = %q, want %q", g, w) + } + if g, w := tok.Extra["id_token"], id; g != w { + t.Errorf("Extra['id_token'] = %q, want %q", g, w) + } + exp := tok.Expiry.Sub(time.Now()) + if (time.Hour-time.Second) > exp || exp > time.Hour { + t.Errorf("Expiry = %v, want ~1 hour", exp) + } +} + +func checkBody(t *testing.T, r *http.Response, body string) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Error("reading reponse body: %v, want %q", err, body) + } + if g, w := string(b), body; g != w { + t.Errorf("request body mismatch: got %q, want %q", g, w) + } +} + +func TestCachePermissions(t *testing.T) { + if runtime.GOOS == "windows" { + // Windows doesn't support file mode bits. + return + } + + td, err := ioutil.TempDir("", "oauth-test") + if err != nil { + t.Fatalf("ioutil.TempDir: %v", err) + } + defer os.RemoveAll(td) + tempFile := filepath.Join(td, "cache-file") + + cf := CacheFile(tempFile) + if err := cf.PutToken(new(Token)); err != nil { + t.Fatalf("PutToken: %v", err) + } + fi, err := os.Stat(tempFile) + if err != nil { + t.Fatalf("os.Stat: %v", err) + } + if fi.Mode()&0077 != 0 { + t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode()) + } +}