forked from Mirrors/oauth2
Initial commit
This commit is contained in:
commit
c32debaa6f
|
@ -0,0 +1,95 @@
|
|||
// Package google provides support for making
|
||||
// OAuth2 authorized and authenticated HTTP requests
|
||||
// to Google APIs. It supports the following
|
||||
// authorization and authentications flows:
|
||||
// - Web Server
|
||||
// - Client-side
|
||||
// - Service Accounts
|
||||
// - Auth from Google Compute Engine
|
||||
// - Auth from Google App Engine
|
||||
//
|
||||
// For more information, please read
|
||||
// https://developers.google.com/accounts/docs/OAuth2.
|
||||
//
|
||||
// Example usage:
|
||||
// // Web server flow usage:
|
||||
// // Specify your configuration.
|
||||
// // Your credentials should be obtained from the Google
|
||||
// // Developer Console (https://console.developers.google.com).
|
||||
// var config = google.NewConfig(&oauth2.Opts{
|
||||
// ClientID: YOUR_CLIENT_ID,
|
||||
// ClientSecret: YOUR_CLIENT_SECRET,
|
||||
// RedirectURL: "http://you.example.org/handler",
|
||||
// Scopes: []string{ "scope1", "scope2" },
|
||||
// })
|
||||
//
|
||||
// // A landing page redirects to Google to get the auth code.
|
||||
// func landing(w http.ResponseWriter, r *http.Request) {
|
||||
// http.Redirect(w, r, config.AuthCodeURL(""), http.StatusFound)
|
||||
// }
|
||||
//
|
||||
// // The user will be redirected back to this handler, that takes the
|
||||
// // "code" query parameter and Exchanges it for an access token.
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// t, err := config.NewTransportWithCode(r.FormValue("code"))
|
||||
// // The Transport now has a valid Token. Create an *http.Client
|
||||
// // with which we can make authenticated API requests.
|
||||
// c := t.Client()
|
||||
// c.Post(...)
|
||||
// }
|
||||
//
|
||||
// // Service accounts usage:
|
||||
// // Google Developer Console will provide a p12 file contains
|
||||
// // a private key. You need to export it to the pem format.
|
||||
// // Run the following command to generate a pem file that
|
||||
// // contains your private key:
|
||||
// // $ openssl pkcs12 -in /path/to/p12key.p12 -out key.pem -nodes
|
||||
// // Then, specify your configuration.
|
||||
// var config = google.NewServiceAccountConfig(&oauth2.JWTOpts{
|
||||
// Email: "xxx@developer.gserviceaccount.com",
|
||||
// PemFilename: "/path/to/key.pem",
|
||||
// Scopes: []string{
|
||||
// "https://www.googleapis.com/auth/drive.readonly"
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// // Create a transport.
|
||||
// t, err := config.NewTransport()
|
||||
// // Or, you can create a transport that impersonates
|
||||
// // a Google user.
|
||||
// t, err := config.NewTransportWithUser(googleUserEmail)
|
||||
//
|
||||
// // Create a client to make authorized requests.
|
||||
// c := t.Client()
|
||||
// c.Post(...)
|
||||
//
|
||||
package google
|
||||
|
||||
import (
|
||||
"github.com/rakyll/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// Google endpoints.
|
||||
uriGoogleAuth = "https://accounts.google.com/o/oauth2/auth"
|
||||
uriGoogleToken = "https://accounts.google.com/o/oauth2/token"
|
||||
)
|
||||
|
||||
// NewConfig creates a new OAuth2 config that uses Google
|
||||
// endpoints.
|
||||
func NewConfig(opts *oauth2.Options) (oauth2.Config, error) {
|
||||
return oauth2.NewConfig(opts, uriGoogleAuth, uriGoogleToken)
|
||||
}
|
||||
|
||||
// NewComputeEngineConfig creates a new config that can fetch tokens
|
||||
// from Google Compute Engine instance's metaserver.
|
||||
func NewComputeEngineConfig() (oauth2.Config, error) {
|
||||
// Should fetch an access token from the meta server.
|
||||
panic("not yet implemented")
|
||||
}
|
||||
|
||||
// NewServiceAccountConfig creates a new JWT config that can
|
||||
// fetch Bearer JWT tokens from Google endpoints.
|
||||
func NewServiceAccountConfig(opts *oauth2.JWTOptions) (oauth2.JWTConfig, error) {
|
||||
return oauth2.NewJWTConfig(opts, uriGoogleToken)
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
// Package jws provides encoding and decoding utilities for
|
||||
// signed JWS messages.
|
||||
package jws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The JWT claim set contains information about the JWT including the
|
||||
// permissions being requested (scopes), the target of the token, the issuer,
|
||||
// the time the token was issued, and the lifetime of the token.
|
||||
type ClaimSet struct {
|
||||
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
|
||||
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
|
||||
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
|
||||
Exp int64 `json:"exp"` // the expiration time of the assertion
|
||||
Iat int64 `json:"iat"` // the time the assertion was issued.
|
||||
Typ string `json:"typ,omitempty"` // token type (Optional).
|
||||
|
||||
// Email for which the application is requesting delegated access (Optional).
|
||||
Sub string `json:"sub,omitempty"`
|
||||
|
||||
// The old name of Sub. Client keeps setting Prn to be
|
||||
// complaint with legacy OAuth 2.0 providers. (Optional)
|
||||
Prn string `json:"prn,omitempty"`
|
||||
|
||||
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
|
||||
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
|
||||
PrivateClaims map[string]interface{} `json:"-"`
|
||||
|
||||
exp time.Time
|
||||
iat time.Time
|
||||
}
|
||||
|
||||
func (c *ClaimSet) encode() (string, error) {
|
||||
if c.exp.IsZero() || c.iat.IsZero() {
|
||||
// Reverting time back for machines whose time is not perfectly in sync.
|
||||
// If client machine's time is in the future according
|
||||
// to Google servers, an access token will not be issued.
|
||||
now := time.Now().Add(-10 * time.Second)
|
||||
c.iat = now
|
||||
c.exp = now.Add(time.Hour)
|
||||
}
|
||||
|
||||
c.Exp = c.exp.Unix()
|
||||
c.Iat = c.iat.Unix()
|
||||
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(c.PrivateClaims) == 0 {
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Marshal private claim set and then append it to b.
|
||||
prv, err := json.Marshal(c.PrivateClaims)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Invalid map of private claims %v", c.PrivateClaims)
|
||||
}
|
||||
|
||||
// Concatenate public and private claim JSON objects.
|
||||
if !bytes.HasSuffix(b, []byte{'}'}) {
|
||||
return "", fmt.Errorf("Invalid JSON %s", b)
|
||||
}
|
||||
if !bytes.HasPrefix(prv, []byte{'{'}) {
|
||||
return "", fmt.Errorf("Invalid JSON %s", prv)
|
||||
}
|
||||
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
|
||||
b = append(b, prv[1:]...) // Append private claims.
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Header represents the header for the signed JWS payloads.
|
||||
type Header struct {
|
||||
// The algorithm used for signature.
|
||||
Algorithm string `json:"alg"`
|
||||
|
||||
// Represents the token type.
|
||||
Typ string `json:"typ"`
|
||||
}
|
||||
|
||||
func (h *Header) encode() (string, error) {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64Encode(b), nil
|
||||
}
|
||||
|
||||
// Decode decodes a claim set from a JWS payload.
|
||||
func Decode(payload string) (c *ClaimSet, err error) {
|
||||
// decode returned id token to get expiry
|
||||
s := strings.Split(payload, ".")
|
||||
if len(s) < 2 {
|
||||
// TODO(jbd): Provide more context about the error.
|
||||
return nil, errors.New("invalid token received")
|
||||
}
|
||||
decoded, err := base64Decode(s[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c = &ClaimSet{}
|
||||
err = json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Encode encodes a signed JWS with provided header and claim set.
|
||||
func Encode(header *Header, c *ClaimSet, signature []byte) (payload string, err error) {
|
||||
var encodedHeader, encodedClaimSet string
|
||||
encodedHeader, err = header.encode()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encodedClaimSet, err = c.encode()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ss := fmt.Sprintf("%s.%s", encodedHeader, encodedClaimSet)
|
||||
parsed, err := parsePrivateKey(signature)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(ss))
|
||||
b, err := rsa.SignPKCS1v15(rand.Reader, parsed, crypto.SHA256, h.Sum(nil))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sig := base64Encode(b)
|
||||
return fmt.Sprintf("%s.%s", ss, sig), nil
|
||||
}
|
||||
|
||||
// base64Encode returns and Base64url encoded version of the input string with any
|
||||
// trailing "=" stripped.
|
||||
func base64Encode(b []byte) string {
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
|
||||
}
|
||||
|
||||
// base64Decode decodes the Base64url encoded string
|
||||
func base64Decode(s string) ([]byte, error) {
|
||||
// add back missing padding
|
||||
switch len(s) % 4 {
|
||||
case 2:
|
||||
s += "=="
|
||||
case 3:
|
||||
s += "="
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
// parsePrivateKey parses the key to extract the private key.
|
||||
// It returns an error if private key is not provided or the
|
||||
// provided key is invalid.
|
||||
func parsePrivateKey(key []byte) (*rsa.PrivateKey, error) {
|
||||
invalidPrivateKeyErr := errors.New("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
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rakyll/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 {
|
||||
// ClientID is the OAuth client identifier used when communicating with
|
||||
// the configured OAuth provider.
|
||||
Email string `json:"email"`
|
||||
|
||||
// The path to the pem file. 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"`
|
||||
}
|
||||
|
||||
// TODO(jbd): Add p12 support.
|
||||
|
||||
// NewJWTConfig creates a new configuration with the specified options
|
||||
// and OAuth2 provider endpoint.
|
||||
func NewJWTConfig(opts *JWTOptions, aud string) (conf JWTConfig, err error) {
|
||||
contents, err := ioutil.ReadFile(opts.PemFilename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conf = &jwtConfig{opts: opts, aud: aud, signature: contents}
|
||||
return
|
||||
}
|
||||
|
||||
type jwtConfig struct {
|
||||
opts *JWTOptions
|
||||
aud string
|
||||
signature []byte
|
||||
}
|
||||
|
||||
// NewTransport creates a transport that is authorize with the
|
||||
// parent JWT configuration.
|
||||
func (c *jwtConfig) NewTransport() (Transport, error) {
|
||||
return &authorizedTransport{fetcher: c, token: &Token{}}, nil
|
||||
}
|
||||
|
||||
// NewTransportWithUser creates a transport that is authorized by
|
||||
// the client and impersonates the specified user.
|
||||
func (c *jwtConfig) NewTransportWithUser(user string) (Transport, error) {
|
||||
return &authorizedTransport{fetcher: c, token: &Token{Subject: user}}, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
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.signature)
|
||||
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.
|
||||
client := http.Client{Transport: DefaultTransport}
|
||||
resp, err := client.PostForm(c.aud, 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
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
// Package oauth2 provides support for making
|
||||
// OAuth2 authorized and authenticated HTTP requests.
|
||||
// It can additionally grant authorization with Bearer JWT.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Specify your configuration. (typically as a global variable)
|
||||
// var config = oauth2.NewConfig(&oauth2.Options{
|
||||
// ClientID: YOUR_CLIENT_ID,
|
||||
// ClientSecret: YOUR_CLIENT_SECRET,
|
||||
// RedirectURL: "http://you.example.org/handler",
|
||||
// Scopes: []string{ "scope1", "scope2" },
|
||||
// }, OAUTH2_PROVIDER_AUTH_URL, OAUTH2_PROVIDER_TOKEN_URL)
|
||||
//
|
||||
// // A landing page redirects to the OAuth provider to get the auth code.
|
||||
// func landing(w http.ResponseWriter, r *http.Request) {
|
||||
// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound)
|
||||
// }
|
||||
//
|
||||
// // The user will be redirected back to this handler, that takes the
|
||||
// // "code" query parameter and Exchanges it for an access token.
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// t, err := config.NewTransportWithCode(r.FormValue("code"))
|
||||
// // The Transport now has a valid Token. Create an *http.Client
|
||||
// // with which we can make authenticated API requests.
|
||||
// c := t.Client()
|
||||
// c.Post(...)
|
||||
// }
|
||||
//
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The default transport implementation to be used while
|
||||
// making the authorized requests.
|
||||
var DefaultTransport = http.DefaultTransport
|
||||
|
||||
type tokenRespBody struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn time.Duration `json:"expires_in"`
|
||||
IdToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
// tokenFetcher refreshes or fetches a new access token from the
|
||||
// provider. It should return an error if it's not capable of
|
||||
// retrieving a token.
|
||||
type tokenFetcher interface {
|
||||
// fetchToken retrieves a new access token for the provider.
|
||||
// If the implementation doesn't know how to retrieve a new token,
|
||||
// it returns an error.
|
||||
fetchToken(existing *Token) (*Token, error)
|
||||
}
|
||||
|
||||
// Options represents options to provide OAuth 2.0 client credentials
|
||||
// and access level. A sample configuration:
|
||||
//
|
||||
// opts := &oauth2.Options{
|
||||
// ClientID: "<clientID>",
|
||||
// ClientSecret: "ad4364309eff",
|
||||
// RedirectURL: "https://homepage/oauth2callback",
|
||||
// Scopes: []string{"scope1", "scope2"},
|
||||
// AccessType: "offline", // retrieves a refresh token
|
||||
// }
|
||||
//
|
||||
type Options struct {
|
||||
// ClientID is the OAuth client identifier used when communicating with
|
||||
// the configured OAuth provider.
|
||||
ClientID string `json:"client_id"`
|
||||
|
||||
// ClientSecret is the OAuth client secret used when communicating with
|
||||
// the configured OAuth provider.
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
// RedirectURL is the URL to which the user will be returned after
|
||||
// granting (or denying) access.
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
|
||||
// Optional, identifies the level of access being requested.
|
||||
Scopes []string `json:"scopes"`
|
||||
|
||||
// Optional, "online" (default) or "offline", no refresh token if "online"
|
||||
AccessType string `json:"omit"`
|
||||
|
||||
// ApprovalPrompt indicates whether the user should be
|
||||
// re-prompted for consent. If set to "auto" (default) the
|
||||
// user will be prompted only if they haven't previously
|
||||
// granted consent and the code can only be exchanged for an
|
||||
// access token.
|
||||
// If set to "force" the user will always be prompted, and the
|
||||
// code can be exchanged for a refresh token.
|
||||
ApprovalPrompt string `json:"omit"`
|
||||
}
|
||||
|
||||
// Config represents an OAuth 2.0 provider and client options to
|
||||
// provide authorized transports.
|
||||
type Config interface {
|
||||
// NewTransport creates a transport which is configured to be
|
||||
// authorized with the config provided.
|
||||
NewTransport() (Transport, error)
|
||||
|
||||
// NewTransportWithCode creates a transport after going through
|
||||
// the OAuth 2.0 exchange flow to retrieve a valid token from
|
||||
// the exchange server.
|
||||
NewTransportWithCode(exchangeCode string) (Transport, error)
|
||||
|
||||
// AuthCodeURL generates a URL to the consent page.
|
||||
AuthCodeURL(state string) (string, error)
|
||||
|
||||
// Exchange ecxhanges the code with the provider to retrieve
|
||||
// a new access token.
|
||||
Exchange(exchangeCode string) (*Token, error)
|
||||
|
||||
// TODO(jbd): Token fetcher strategy should be settable
|
||||
// from external packages.
|
||||
}
|
||||
|
||||
// Config represents an OAuth 2.0 provider and client options to
|
||||
// provide authorized transports with a Bearer JWT token.
|
||||
type JWTConfig interface {
|
||||
// NewTransport creates a transport which is configured to
|
||||
// be authorized with OAuth 2.0 JWT Bearer flow.
|
||||
NewTransport() (Transport, error)
|
||||
|
||||
// NewTransportWithUser creates a transport which is configured
|
||||
// to be authorized with OAuth 2.0 JWT Bearer flow and
|
||||
// impersonates the provided user.
|
||||
NewTransportWithUser(user string) (Transport, error)
|
||||
|
||||
// TODO(jbd): Token fetcher strategy should be settable
|
||||
// from external packages.
|
||||
}
|
||||
|
||||
// NewConfig creates a generic OAuth 2.0 configuration that talks
|
||||
// to an OAuth 2.0 provider specified with authURL and tokenURL.
|
||||
func NewConfig(opts *Options, authURL, tokenURL string) (Config, error) {
|
||||
conf := &config{
|
||||
opts: opts,
|
||||
authURL: authURL,
|
||||
tokenURL: tokenURL,
|
||||
}
|
||||
if err := conf.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// config represent the configuration of an OAuth 2.0 consumer client.
|
||||
type config struct {
|
||||
opts *Options
|
||||
// AuthURL is the URL the user will be directed to
|
||||
// in order to grant access.
|
||||
authURL string
|
||||
// TokenURL is the URL used to retrieve OAuth tokens.
|
||||
tokenURL string
|
||||
}
|
||||
|
||||
// AuthCodeURL returns a URL to OAuth 2.0 provider's consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
func (c *config) AuthCodeURL(state string) (authURL string, err error) {
|
||||
u, err := url.Parse(c.authURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
q := url.Values{
|
||||
"response_type": {"code"},
|
||||
"client_id": {c.opts.ClientID},
|
||||
"redirect_uri": {c.opts.RedirectURL},
|
||||
"scope": {strings.Join(c.opts.Scopes, " ")},
|
||||
"state": {state},
|
||||
"access_type": {c.opts.AccessType},
|
||||
"approval_prompt": {c.opts.ApprovalPrompt},
|
||||
}.Encode()
|
||||
if u.RawQuery == "" {
|
||||
u.RawQuery = q
|
||||
} else {
|
||||
u.RawQuery += "&" + q
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// NewTransport creates a new authorizable transport. It doesn't
|
||||
// initialize the new transport with a token, so after creation,
|
||||
// you need to set a valid token (or an expired token with a valid
|
||||
// refresh token) in order to be able to do authorized requests.
|
||||
//
|
||||
// Example:
|
||||
// t, _ := c.NewTransport()
|
||||
// t.SetToken(validToken)
|
||||
//
|
||||
func (c *config) NewTransport() (Transport, error) {
|
||||
return &authorizedTransport{fetcher: c}, nil
|
||||
}
|
||||
|
||||
// NewTransportWithCode exchanges the OAuth 2.0 exchange code with
|
||||
// the provider to fetch a new access token (and refresh token). Once
|
||||
// it succesffully retrieves a new token, creates a new transport
|
||||
// authorized with it.
|
||||
func (c *config) NewTransportWithCode(exchangeCode string) (Transport, error) {
|
||||
token, err := c.Exchange(exchangeCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &authorizedTransport{fetcher: c, token: token}, nil
|
||||
}
|
||||
|
||||
// Exchange exchanges the exchange code with the OAuth 2.0 provider
|
||||
// to retrieve a new access token.
|
||||
func (c *config) Exchange(exchangeCode string) (*Token, error) {
|
||||
token := &Token{}
|
||||
err := c.updateToken(token, url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"redirect_uri": {c.opts.RedirectURL},
|
||||
"scope": {strings.Join(c.opts.Scopes, " ")},
|
||||
"code": {exchangeCode},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// fetchToken retrieves a new access token and updates the existing token
|
||||
// with the newly fetched credentials. If existing token doesn't
|
||||
// contain a refresh token, it returns an error.
|
||||
func (c *config) fetchToken(existing *Token) (*Token, error) {
|
||||
if existing == nil || existing.RefreshToken == "" {
|
||||
return nil, errors.New("cannot fetch access token without refresh token.")
|
||||
}
|
||||
err := c.updateToken(existing, url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {existing.RefreshToken},
|
||||
})
|
||||
return existing, err
|
||||
}
|
||||
|
||||
// Checks if all required configuration fields have non-zero values.
|
||||
func (c *config) validate() error {
|
||||
if c.opts.ClientID == "" {
|
||||
return errors.New("A client ID should be provided.")
|
||||
}
|
||||
if c.opts.ClientSecret == "" {
|
||||
return errors.New("A client secret should be provided.")
|
||||
}
|
||||
// TODO(jbd): Are redirect URIs allowed to be a
|
||||
// non-value string in the spec?
|
||||
if c.opts.RedirectURL == "" {
|
||||
return errors.New("A redirect URL should be provided.")
|
||||
}
|
||||
// TODO(jbd): Validate the URLs. Maybe convert them to URL
|
||||
// objects on construction.
|
||||
if c.authURL == "" {
|
||||
return errors.New("An auth URL should be provided.")
|
||||
}
|
||||
if c.tokenURL == "" {
|
||||
return errors.New("A token URL should be provided.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) updateToken(tok *Token, v url.Values) error {
|
||||
v.Set("client_id", c.opts.ClientID)
|
||||
v.Set("client_secret", c.opts.ClientSecret)
|
||||
r, err := (&http.Client{Transport: DefaultTransport}).PostForm(c.tokenURL, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode != 200 {
|
||||
// TODO(jbd): Add status code or error message
|
||||
return errors.New("Error during updating token.")
|
||||
}
|
||||
|
||||
resp := &tokenRespBody{}
|
||||
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
switch content {
|
||||
case "application/x-www-form-urlencoded", "text/plain":
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vals, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.AccessToken = vals.Get("access_token")
|
||||
resp.TokenType = vals.Get("token_type")
|
||||
resp.RefreshToken = vals.Get("refresh_token")
|
||||
resp.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s")
|
||||
resp.IdToken = vals.Get("id_token")
|
||||
default:
|
||||
if err = json.NewDecoder(r.Body).Decode(&resp); err != nil {
|
||||
return err
|
||||
}
|
||||
// The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above,
|
||||
// so compensate here.
|
||||
resp.ExpiresIn *= time.Second
|
||||
}
|
||||
tok.AccessToken = resp.AccessToken
|
||||
tok.TokenType = resp.TokenType
|
||||
// Don't overwrite `RefreshToken` with an empty value
|
||||
if resp.RefreshToken == "" {
|
||||
tok.RefreshToken = resp.RefreshToken
|
||||
}
|
||||
if resp.ExpiresIn == 0 {
|
||||
tok.Expiry = time.Time{}
|
||||
} else {
|
||||
tok.Expiry = time.Now().Add(resp.ExpiresIn)
|
||||
}
|
||||
if resp.IdToken != "" {
|
||||
if tok.Extra == nil {
|
||||
tok.Extra = make(map[string]string)
|
||||
}
|
||||
tok.Extra["id_token"] = resp.IdToken
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
rt func(req *http.Request) (resp *http.Response, err error)
|
||||
}
|
||||
|
||||
func (t *mockTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
return t.rt(req)
|
||||
}
|
||||
|
||||
func newTestConf() Config {
|
||||
conf, _ := NewConfig(&Options{
|
||||
ClientID: "CLIENT_ID",
|
||||
ClientSecret: "CLIENT_SECRET",
|
||||
RedirectURL: "REDIRECT_URL",
|
||||
Scopes: []string{
|
||||
"scope1",
|
||||
"scope2",
|
||||
},
|
||||
AccessType: "offline",
|
||||
ApprovalPrompt: "force",
|
||||
}, "auth-url", "token-url")
|
||||
return conf
|
||||
}
|
||||
|
||||
func TestAuthCodeURL(t *testing.T) {
|
||||
DefaultTransport = http.DefaultTransport
|
||||
|
||||
conf := newTestConf()
|
||||
url, err := conf.AuthCodeURL("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to generate an auth URL, failed with %v.", err)
|
||||
}
|
||||
if url != "auth-url?access_type=offline&approval_prompt=force&client_id=CLIENT_ID&redirect_uri=REDIRECT_URL&response_type=code&scope=scope1+scope2&state=foo" {
|
||||
t.Fatalf("Generated auth URL is not the expected. Found %v.", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangePayload(t *testing.T) {
|
||||
conf := newTestConf()
|
||||
DefaultTransport = &mockTransport{
|
||||
rt: func(req *http.Request) (resp *http.Response, err error) {
|
||||
headerContentType := req.Header.Get("Content-Type")
|
||||
if headerContentType != "application/x-www-form-urlencoded" {
|
||||
t.Fatalf("Content-Type header is expected to be application/x-www-form-urlencoded, %v found.", headerContentType)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
if string(body) != "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&code=exchange-code&grant_type=authorization_code&redirect_uri=REDIRECT_URL&scope=scope1+scope2" {
|
||||
t.Fatalf("Exchange payload is found to be %v", string(body))
|
||||
}
|
||||
return nil, errors.New("no response")
|
||||
},
|
||||
}
|
||||
conf.Exchange("exchange-code")
|
||||
}
|
||||
|
||||
func TestExchangingTransport(t *testing.T) {
|
||||
conf := newTestConf()
|
||||
DefaultTransport = &mockTransport{
|
||||
rt: func(req *http.Request) (resp *http.Response, err error) {
|
||||
if req.URL.RequestURI() != "token-url" {
|
||||
t.Fatalf("NewTransportWithCode should have exchanged the code, but it didn't.")
|
||||
}
|
||||
return nil, errors.New("no response")
|
||||
},
|
||||
}
|
||||
conf.NewTransportWithCode("exchange-code")
|
||||
}
|
||||
|
||||
func TestFetchWithNoRedirect(t *testing.T) {
|
||||
DefaultTransport = http.DefaultTransport
|
||||
|
||||
fetcher := newTestConf().(tokenFetcher)
|
||||
_, err := fetcher.fetchToken(&Token{})
|
||||
if err == nil {
|
||||
t.Fatalf("Fetch should return an error if no refresh token is set")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTokenType = "Bearer"
|
||||
)
|
||||
|
||||
// Token represents the crendentials used to authorize
|
||||
// the requests to access protected resources on the OAuth 2.0
|
||||
// provider's backend.
|
||||
type Token struct {
|
||||
// A token that authorizes and authenticates the requests.
|
||||
AccessToken string `json:"access_token"`
|
||||
|
||||
// Identifies the type of token returned.
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
|
||||
// A token that may be used to obtain a new access token.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
|
||||
// The remaining lifetime of the access token.
|
||||
Expiry time.Time `json:"expiry,omitempty"`
|
||||
|
||||
Extra map[string]string `json:"extra,omitempty"`
|
||||
|
||||
// JWT related fields
|
||||
Subject string `json:"subject,omitempty"`
|
||||
}
|
||||
|
||||
// Expired returns true if there is no access token or the
|
||||
// access token is expired.
|
||||
func (t *Token) Expired() bool {
|
||||
if t.AccessToken == "" {
|
||||
return true
|
||||
}
|
||||
if t.Expiry.IsZero() {
|
||||
return false
|
||||
}
|
||||
return t.Expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
// Transport represents an authorized transport.
|
||||
// Provides currently in-use user token and allows to set a token to
|
||||
// be used. If token expires, it tries to fetch a new token,
|
||||
// if possible. Token fetching is thread-safe. If two or more
|
||||
// concurrent requests are being made with the same expired token,
|
||||
// one of the requests will wait for the other to refresh
|
||||
// the existing token.
|
||||
type Transport interface {
|
||||
// Authenticates the request with the existing token. If token is
|
||||
// expired, tries to refresh/fetch a new token.
|
||||
// Makes the request by delegating it to the default transport.
|
||||
RoundTrip(*http.Request) (*http.Response, error)
|
||||
|
||||
// Returns the token authenticates the transport.
|
||||
// This operation is thread-safe.
|
||||
Token() *Token
|
||||
|
||||
// Sets a new token to authenticate the transport.
|
||||
// This operation is thread-safe.
|
||||
SetToken(token *Token)
|
||||
|
||||
// Refreshes the token if refresh is possible (such as in the
|
||||
// presense of a refresh token). Returns an error if refresh is
|
||||
// not possible. Refresh is thread-safe.
|
||||
RefreshToken() error
|
||||
}
|
||||
|
||||
type authorizedTransport struct {
|
||||
fetcher tokenFetcher
|
||||
token *Token
|
||||
|
||||
// Mutex to protect token during auto refreshments.
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// RoundTrip authorizes the request with the existing token.
|
||||
// If token is expired, tries to refresh/fetch a new token.
|
||||
func (t *authorizedTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
token := t.Token()
|
||||
if token == nil || token.Expired() {
|
||||
// Check if the token is refreshable.
|
||||
// If token is refreshable, don't return an error,
|
||||
// rather refresh.
|
||||
|
||||
if err := t.RefreshToken(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token = t.Token()
|
||||
}
|
||||
|
||||
// To set the Authorization header, we must make a copy of the Request
|
||||
// so that we don't modify the Request we were given.
|
||||
// This is required by the specification of http.RoundTripper.
|
||||
req = cloneRequest(req)
|
||||
typ := token.TokenType
|
||||
if typ == "" {
|
||||
typ = defaultTokenType
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", typ+" "+token.AccessToken)
|
||||
|
||||
// Make the HTTP request.
|
||||
return DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// Token returns the existing token that authorizes the Transport.
|
||||
func (t *authorizedTransport) Token() *Token {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
if t.token == nil {
|
||||
return nil
|
||||
}
|
||||
token := &Token{
|
||||
AccessToken: t.token.AccessToken,
|
||||
TokenType: t.token.TokenType,
|
||||
RefreshToken: t.token.RefreshToken,
|
||||
Expiry: t.token.Expiry,
|
||||
Extra: t.token.Extra,
|
||||
Subject: t.token.Subject,
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
// SetToken sets a token to the transport in a thread-safe way.
|
||||
func (t *authorizedTransport) SetToken(token *Token) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
t.token = token
|
||||
}
|
||||
|
||||
// RefreshToken retrieves a new token, if a refreshing/fetching
|
||||
// method is known and required credentials are presented
|
||||
// (such as a refresh token).
|
||||
func (t *authorizedTransport) RefreshToken() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
token, err := t.fetcher.fetchToken(t.token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header)
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = s
|
||||
}
|
||||
return r2
|
||||
}
|
Loading…
Reference in New Issue