// Copyright 2014 The Go 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 jwt implements the OAuth 2.0 JSON Web Token flow, commonly // known as "two-legged OAuth 2.0". // // See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 package jwt import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" "time" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/internal" "golang.org/x/oauth2/jws" ) var ( defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} ) // Config is the configuration for using JWT to fetch tokens, // commonly known as "two-legged OAuth 2.0". type Config struct { // Email is the OAuth client identifier used when communicating with // the configured OAuth provider. Email string // 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 // Subject is the optional user to impersonate. Subject string // Scopes optionally specifies a list of requested permission scopes. Scopes []string // TokenURL is the endpoint required to complete the 2-legged JWT flow. TokenURL string // Expires optionally specifies how long the token is valid for. Expires time.Duration } // TokenSource returns a JWT TokenSource using the configuration // in c and the HTTP client from the provided context. func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) } // Client returns an HTTP client wrapping the context's // HTTP transport and adding Authorization headers with tokens // obtained from c. // // The returned client and its Transport should not be modified. func (c *Config) Client(ctx context.Context) *http.Client { return oauth2.NewClient(ctx, c.TokenSource(ctx)) } // jwtSource is a source that always does a signed JWT request for a token. // It should typically be wrapped with a reuseTokenSource. type jwtSource struct { ctx context.Context conf *Config } func (js jwtSource) Token() (*oauth2.Token, error) { pk, err := internal.ParseKey(js.conf.PrivateKey) if err != nil { return nil, err } hc := oauth2.NewClient(js.ctx, nil) claimSet := &jws.ClaimSet{ Iss: js.conf.Email, Scope: strings.Join(js.conf.Scopes, " "), Aud: js.conf.TokenURL, } if subject := js.conf.Subject; subject != "" { claimSet.Sub = subject // prn is the old name of sub. Keep setting it // to be compatible with legacy OAuth 2.0 providers. claimSet.Prn = subject } if t := js.conf.Expires; t > 0 { claimSet.Exp = time.Now().Add(t).Unix() } payload, err := jws.Encode(defaultHeader, claimSet, pk) if err != nil { return nil, err } v := url.Values{} v.Set("grant_type", defaultGrantType) v.Set("assertion", payload) resp, err := hc.PostForm(js.conf.TokenURL, v) if err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) 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) } // tokenRes is the JSON response body. var tokenRes struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` IDToken string `json:"id_token"` ExpiresIn int64 `json:"expires_in"` // relative seconds from now } if err := json.Unmarshal(body, &tokenRes); err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } token := &oauth2.Token{ AccessToken: tokenRes.AccessToken, TokenType: tokenRes.TokenType, } raw := make(map[string]interface{}) json.Unmarshal(body, &raw) // no error checks for optional fields token = token.WithExtra(raw) if secs := tokenRes.ExpiresIn; secs > 0 { token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) } if v := tokenRes.IDToken; v != "" { // decode returned id token to get expiry claimSet, err := jws.Decode(v) if err != nil { return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err) } token.Expiry = time.Unix(claimSet.Exp, 0) } return token, nil }