blob: 78d9553dfda4936c413c0e933787fb0164022f69 [file] [log] [blame]
// Copyright 2014 The oauth2 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 oauth2
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang/oauth2/jws"
)
var (
defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"}
)
// JWTOptions represents a OAuth2 client's crendentials to retrieve a
// Bearer JWT token.
type JWTOptions struct {
// Email is the OAuth client identifier used when communicating with
// the configured OAuth provider.
Email string `json:"email"`
// 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 `json:"-"`
// Scopes identify the level of access being requested.
Scopes []string `json:"scopes"`
}
// NewJWTConfig creates a new configuration with the specified options
// and OAuth2 provider endpoint.
func NewJWTConfig(opts *JWTOptions, aud string) (*JWTConfig, error) {
audURL, err := url.Parse(aud)
if err != nil {
return nil, err
}
parsedKey, err := parseKey(opts.PrivateKey)
if err != nil {
return nil, err
}
return &JWTConfig{
opts: opts,
aud: audURL,
key: parsedKey,
}, nil
}
// JWTConfig represents an OAuth 2.0 provider and client options to
// provide authorized transports with a Bearer JWT token.
type JWTConfig struct {
// Client is the HTTP client to be used to retrieve
// tokens from the OAuth 2.0 provider.
Client *http.Client
// Transport is the http.RoundTripper to be used
// to construct new oauth2.Transport instances from
// this configuration.
Transport http.RoundTripper
opts *JWTOptions
aud *url.URL
key *rsa.PrivateKey
}
// NewTransport creates a transport that is authorize with the
// parent JWT configuration.
func (c *JWTConfig) NewTransport() *Transport {
return NewTransport(c.transport(), c, &Token{})
}
// NewTransportWithUser creates a transport that is authorized by
// the client and impersonates the specified user.
func (c *JWTConfig) NewTransportWithUser(user string) *Transport {
return NewTransport(c.transport(), c, &Token{Subject: user})
}
// fetchToken retrieves a new access token and updates the existing token
// with the newly fetched credentials.
func (c *JWTConfig) FetchToken(existing *Token) (*Token, error) {
if existing == nil {
existing = &Token{}
}
claimSet := &jws.ClaimSet{
Iss: c.opts.Email,
Scope: strings.Join(c.opts.Scopes, " "),
Aud: c.aud.String(),
}
if existing.Subject != "" {
claimSet.Sub = existing.Subject
// prn is the old name of sub. Keep setting it
// to be compatible with legacy OAuth 2.0 providers.
claimSet.Prn = existing.Subject
}
payload, err := jws.Encode(defaultHeader, claimSet, c.key)
if err != nil {
return nil, err
}
v := url.Values{}
v.Set("grant_type", defaultGrantType)
v.Set("assertion", payload)
// Make a request with assertion to get a new token.
resp, err := c.client().PostForm(c.aud.String(), v)
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
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)
}
b := &tokenRespBody{}
if err := json.Unmarshal(body, b); err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}
token := &Token{
AccessToken: b.AccessToken,
TokenType: b.TokenType,
Subject: existing.Subject,
}
if b.IdToken != "" {
// decode returned id token to get expiry
claimSet := &jws.ClaimSet{}
claimSet, err = jws.Decode(b.IdToken)
if err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
}
token.Expiry = time.Unix(claimSet.Exp, 0)
return token, nil
}
token.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second)
return token, nil
}
func (c *JWTConfig) transport() http.RoundTripper {
if c.Transport != nil {
return c.Transport
}
return http.DefaultTransport
}
func (c *JWTConfig) client() *http.Client {
if c.Client != nil {
return c.Client
}
return http.DefaultClient
}
// parseKey converts the binary contents of a private key file
// to an *rsa.PrivateKey. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func parseKey(key []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(key)
if block != nil {
key = block.Bytes
}
parsedKey, err := x509.ParsePKCS8PrivateKey(key)
if err != nil {
parsedKey, err = x509.ParsePKCS1PrivateKey(key)
if err != nil {
return nil, err
}
}
parsed, ok := parsedKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("oauth2: private key is invalid")
}
return parsed, nil
}