forked from Mirrors/oauth2
180 lines
4.7 KiB
Go
180 lines
4.7 KiB
Go
// 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"
|
|
"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 is an RSA private key to sign JWS payloads.
|
|
PrivateKey *rsa.PrivateKey `json:"-"`
|
|
|
|
// The path to a PEM container that includes your private key.
|
|
// If PrivateKey is set, this field is ignored.
|
|
//
|
|
// If you have a p12 file instead, you
|
|
// can use `openssl` to export the private key into a PEM file.
|
|
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
|
// PEM file should contain your private key.
|
|
PEMFilename string `json:"pemfilename"`
|
|
|
|
// 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
|
|
}
|
|
if opts.PrivateKey != nil {
|
|
return &JWTConfig{opts: opts, aud: audURL, key: opts.PrivateKey}, nil
|
|
}
|
|
contents, err := ioutil.ReadFile(opts.PEMFilename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsedKey, err := parsePemKey(contents)
|
|
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 {
|
|
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 NewAuthorizedTransport(http.DefaultTransport, 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 NewAuthorizedTransport(http.DefaultTransport, 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 *Token, err 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
|
|
}
|
|
v := url.Values{}
|
|
v.Set("grant_type", defaultGrantType)
|
|
v.Set("assertion", payload)
|
|
|
|
// Make a request with assertion to get a new token.
|
|
resp, err := http.DefaultClient.PostForm(c.aud.String(), v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
// TODO(jbd): Provide more context about the response.
|
|
return nil, errors.New("Cannot fetch token, response: " + resp.Status)
|
|
}
|
|
|
|
b := &tokenRespBody{}
|
|
err = json.NewDecoder(resp.Body).Decode(b)
|
|
if err != nil {
|
|
return nil, 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
|
|
}
|
|
token.Expiry = time.Unix(claimSet.Exp, 0)
|
|
return
|
|
}
|
|
|
|
token.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second)
|
|
return
|
|
}
|
|
|
|
// parsePemKey parses the pem file to extract the private key.
|
|
// It returns an error if private key is not provided or the
|
|
// provided key is invalid.
|
|
func parsePemKey(key []byte) (*rsa.PrivateKey, error) {
|
|
invalidPrivateKeyErr := errors.New("oauth2: private key is invalid")
|
|
block, _ := pem.Decode(key)
|
|
if block == nil {
|
|
return nil, invalidPrivateKeyErr
|
|
}
|
|
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
parsed, ok := parsedKey.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, invalidPrivateKeyErr
|
|
}
|
|
return parsed, nil
|
|
}
|