Initial commit

This commit is contained in:
Burcu Dogan 2014-05-05 23:54:23 +02:00
commit c32debaa6f
7 changed files with 997 additions and 0 deletions

0
README.md Normal file
View File

95
google/google.go Normal file
View File

@ -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)
}

189
jws/jws.go Normal file
View File

@ -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
}

135
jwt.go Normal file
View File

@ -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
}

327
oauth2.go Normal file
View File

@ -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
}

85
oauth2_test.go Normal file
View File

@ -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")
}
}

166
transport.go Normal file
View File

@ -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
}