forked from gitea/gitea
186 lines
5.4 KiB
Go
186 lines
5.4 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
|
|
|
|
// PrivateClaims optionally specifies custom private claims in the JWT.
|
|
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
|
PrivateClaims map[string]interface{}
|
|
|
|
// UseIDToken optionally specifies whether ID token should be used instead
|
|
// of access token when the server returns both.
|
|
UseIDToken bool
|
|
}
|
|
|
|
// 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,
|
|
PrivateClaims: js.conf.PrivateClaims,
|
|
}
|
|
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)
|
|
}
|
|
if js.conf.UseIDToken {
|
|
if tokenRes.IDToken == "" {
|
|
return nil, fmt.Errorf("oauth2: response doesn't have JWT token")
|
|
}
|
|
token.AccessToken = tokenRes.IDToken
|
|
}
|
|
return token, nil
|
|
}
|