| // 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 ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "net/url" |
| "strings" |
| "time" |
| |
| "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 |
| |
| // PrivateKeyID contains an optional hint indicating which key is being |
| // used. |
| PrivateKeyID string |
| |
| // 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 |
| |
| // Audience optionally specifies the intended audience of the |
| // request. If empty, the value of TokenURL is used as the |
| // intended audience. |
| Audience string |
| } |
| |
| // 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() |
| } |
| if aud := js.conf.Audience; aud != "" { |
| claimSet.Aud = aud |
| } |
| h := *defaultHeader |
| h.KeyID = js.conf.PrivateKeyID |
| payload, err := jws.Encode(&h, 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, &oauth2.RetrieveError{ |
| Response: resp, |
| Body: 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 |
| } |