// Copyright 2014 The oauth2 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 oauth2 import ( "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "errors" "fmt" "io/ioutil" "net/http" "net/url" "strings" "time" "github.com/golang/oauth2/jws" ) var ( defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} ) // JWTOptions represents a OAuth2 client's crendentials to retrieve a // Bearer JWT token. type JWTOptions struct { // Email is the OAuth client identifier used when communicating with // the configured OAuth provider. Email string `json:"email"` // PrivateKey contains the contents of an RSA private key or the // contents of a PEM file that contains a private key. The provided // private key is used to sign JWT payloads. // PEM containers with a passphrase are not supported. // Use the following command to convert a PKCS 12 file into a PEM. // // $ openssl pkcs12 -in key.p12 -out key.pem -nodes // PrivateKey []byte `json:"-"` // Scopes identify the level of access being requested. Scopes []string `json:"scopes"` } // NewJWTConfig creates a new configuration with the specified options // and OAuth2 provider endpoint. func NewJWTConfig(opts *JWTOptions, aud string) (*JWTConfig, error) { audURL, err := url.Parse(aud) if err != nil { return nil, err } parsedKey, err := parseKey(opts.PrivateKey) if err != nil { return nil, err } return &JWTConfig{ opts: opts, aud: audURL, key: parsedKey, }, nil } // JWTConfig represents an OAuth 2.0 provider and client options to // provide authorized transports with a Bearer JWT token. type JWTConfig struct { // Client is the HTTP client to be used to retrieve // tokens from the OAuth 2.0 provider. Client *http.Client // Transport is the http.RoundTripper to be used // to construct new oauth2.Transport instances from // this configuration. Transport http.RoundTripper opts *JWTOptions aud *url.URL key *rsa.PrivateKey } // NewTransport creates a transport that is authorize with the // parent JWT configuration. func (c *JWTConfig) NewTransport() *Transport { return NewTransport(c.transport(), c, &Token{}) } // NewTransportWithUser creates a transport that is authorized by // the client and impersonates the specified user. func (c *JWTConfig) NewTransportWithUser(user string) *Transport { return NewTransport(c.transport(), c, &Token{Subject: user}) } // fetchToken retrieves a new access token and updates the existing token // with the newly fetched credentials. func (c *JWTConfig) FetchToken(existing *Token) (*Token, error) { if existing == nil { existing = &Token{} } claimSet := &jws.ClaimSet{ Iss: c.opts.Email, Scope: strings.Join(c.opts.Scopes, " "), Aud: c.aud.String(), } if existing.Subject != "" { claimSet.Sub = existing.Subject // prn is the old name of sub. Keep setting it // to be compatible with legacy OAuth 2.0 providers. claimSet.Prn = existing.Subject } payload, err := jws.Encode(defaultHeader, claimSet, c.key) if err != nil { return nil, err } v := url.Values{} v.Set("grant_type", defaultGrantType) v.Set("assertion", payload) // Make a request with assertion to get a new token. resp, err := c.client().PostForm(c.aud.String(), v) if err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", resp.Status, body) } b := &tokenRespBody{} if err := json.Unmarshal(body, b); err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } token := &Token{ AccessToken: b.AccessToken, TokenType: b.TokenType, Subject: existing.Subject, } if b.IdToken != "" { // decode returned id token to get expiry claimSet := &jws.ClaimSet{} claimSet, err = jws.Decode(b.IdToken) if err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } token.Expiry = time.Unix(claimSet.Exp, 0) return token, nil } token.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second) return token, nil } func (c *JWTConfig) transport() http.RoundTripper { if c.Transport != nil { return c.Transport } return http.DefaultTransport } func (c *JWTConfig) client() *http.Client { if c.Client != nil { return c.Client } return http.DefaultClient } // parseKey converts the binary contents of a private key file // to an *rsa.PrivateKey. It detects whether the private key is in a // PEM container or not. If so, it extracts the the private key // from PEM container before conversion. It only supports PEM // containers with no passphrase. func parseKey(key []byte) (*rsa.PrivateKey, error) { block, _ := pem.Decode(key) if block != nil { key = block.Bytes } parsedKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { parsedKey, err = x509.ParsePKCS1PrivateKey(key) if err != nil { return nil, err } } parsed, ok := parsedKey.(*rsa.PrivateKey) if !ok { return nil, errors.New("oauth2: private key is invalid") } return parsed, nil }