mirror of
https://github.com/golang/oauth2.git
synced 2025-07-21 00:00:09 +08:00
Add an Audience field to jwt.Config which, if set, is used instead of TokenURL as the 'aud' claim in the generated JWT. This allows the jwt package to work with authorization servers that require the 'aud' claim and token endpoint URL to be different values. Fixes #369. Change-Id: I883aabece7f9b16ec726d5bfa98c1ec91876b651 GitHub-Last-Rev: fd73e4d50cfe0450fd59ffc6d4c5db7a3f660b60 GitHub-Pull-Request: golang/oauth2#370 Reviewed-on: https://go-review.googlesource.com/c/162937 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
171 lines
4.9 KiB
Go
171 lines
4.9 KiB
Go
// 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
|
|
}
|