You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
4.7 KiB
162 lines
4.7 KiB
// 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 |
|
|
|
// 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 |
|
} |
|
|
|
// 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() |
|
} |
|
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 |
|
}
|
|
|