mirror of https://github.com/docker/docs.git
Merge pull request #2116 from dgageot/1628-oauth2
Move `goauth2/oauth` dependency to `golang.org/x/oauth2`
This commit is contained in:
commit
34ee11e51d
|
@ -54,11 +54,6 @@
|
|||
"github.com/docker/machine/version"
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "code.google.com/p/goauth2/oauth",
|
||||
"Comment": "weekly-56",
|
||||
"Rev": "afe77d958c701557ec5dc56f6936fcc194d15520"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/MSOpenTech/azure-sdk-for-go",
|
||||
"Comment": "v1.1-17-g515f3ec",
|
||||
|
|
|
@ -5,13 +5,13 @@ import (
|
|||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/goauth2/oauth"
|
||||
"github.com/digitalocean/godo"
|
||||
"github.com/docker/machine/libmachine/drivers"
|
||||
"github.com/docker/machine/libmachine/log"
|
||||
"github.com/docker/machine/libmachine/mcnflag"
|
||||
"github.com/docker/machine/libmachine/ssh"
|
||||
"github.com/docker/machine/libmachine/state"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
|
@ -293,11 +293,11 @@ func (d *Driver) Kill() error {
|
|||
}
|
||||
|
||||
func (d *Driver) getClient() *godo.Client {
|
||||
t := &oauth.Transport{
|
||||
Token: &oauth.Token{AccessToken: d.AccessToken},
|
||||
}
|
||||
token := &oauth2.Token{AccessToken: d.AccessToken}
|
||||
tokenSource := oauth2.StaticTokenSource(token)
|
||||
client := oauth2.NewClient(oauth2.NoContext, tokenSource)
|
||||
|
||||
return godo.NewClient(t.Client())
|
||||
return godo.NewClient(client)
|
||||
}
|
||||
|
||||
func (d *Driver) publicSSHKeyPath() string {
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright 2011 The goauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This program makes a call to the specified API, authenticated with OAuth2.
|
||||
// a list of example APIs can be found at https://code.google.com/oauthplayground/
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"code.google.com/p/goauth2/oauth"
|
||||
)
|
||||
|
||||
var (
|
||||
clientId = flag.String("id", "", "Client ID")
|
||||
clientSecret = flag.String("secret", "", "Client Secret")
|
||||
scope = flag.String("scope", "https://www.googleapis.com/auth/userinfo.profile", "OAuth scope")
|
||||
redirectURL = flag.String("redirect_url", "oob", "Redirect URL")
|
||||
authURL = flag.String("auth_url", "https://accounts.google.com/o/oauth2/auth", "Authentication URL")
|
||||
tokenURL = flag.String("token_url", "https://accounts.google.com/o/oauth2/token", "Token URL")
|
||||
requestURL = flag.String("request_url", "https://www.googleapis.com/oauth2/v1/userinfo", "API request")
|
||||
code = flag.String("code", "", "Authorization Code")
|
||||
cachefile = flag.String("cache", "cache.json", "Token cache file")
|
||||
)
|
||||
|
||||
const usageMsg = `
|
||||
To obtain a request token you must specify both -id and -secret.
|
||||
|
||||
To obtain Client ID and Secret, see the "OAuth 2 Credentials" section under
|
||||
the "API Access" tab on this page: https://code.google.com/apis/console/
|
||||
|
||||
Once you have completed the OAuth flow, the credentials should be stored inside
|
||||
the file specified by -cache and you may run without the -id and -secret flags.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Set up a configuration.
|
||||
config := &oauth.Config{
|
||||
ClientId: *clientId,
|
||||
ClientSecret: *clientSecret,
|
||||
RedirectURL: *redirectURL,
|
||||
Scope: *scope,
|
||||
AuthURL: *authURL,
|
||||
TokenURL: *tokenURL,
|
||||
TokenCache: oauth.CacheFile(*cachefile),
|
||||
}
|
||||
|
||||
// Set up a Transport using the config.
|
||||
transport := &oauth.Transport{Config: config}
|
||||
|
||||
// Try to pull the token from the cache; if this fails, we need to get one.
|
||||
token, err := config.TokenCache.Token()
|
||||
if err != nil {
|
||||
if *clientId == "" || *clientSecret == "" {
|
||||
flag.Usage()
|
||||
fmt.Fprint(os.Stderr, usageMsg)
|
||||
os.Exit(2)
|
||||
}
|
||||
if *code == "" {
|
||||
// Get an authorization code from the data provider.
|
||||
// ("Please ask the user if I can access this resource.")
|
||||
url := config.AuthCodeURL("")
|
||||
fmt.Print("Visit this URL to get a code, then run again with -code=YOUR_CODE\n\n")
|
||||
fmt.Println(url)
|
||||
return
|
||||
}
|
||||
// Exchange the authorization code for an access token.
|
||||
// ("Here's the code you gave the user, now give me a token!")
|
||||
token, err = transport.Exchange(*code)
|
||||
if err != nil {
|
||||
log.Fatal("Exchange:", err)
|
||||
}
|
||||
// (The Exchange method will automatically cache the token.)
|
||||
fmt.Printf("Token is cached in %v\n", config.TokenCache)
|
||||
}
|
||||
|
||||
// Make the actual request using the cached token to authenticate.
|
||||
// ("Here's the token, let me in!")
|
||||
transport.Token = token
|
||||
|
||||
// Make the request.
|
||||
r, err := transport.Client().Get(*requestURL)
|
||||
if err != nil {
|
||||
log.Fatal("Get:", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Write the response to standard output.
|
||||
io.Copy(os.Stdout, r.Body)
|
||||
|
||||
// Send final carriage return, just to be neat.
|
||||
fmt.Println()
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"XXXXXXXXXXXX@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXX@developer.gserviceaccount.com","client_id":"XXXXXXXXXXXX.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}
|
|
@ -1,20 +0,0 @@
|
|||
Bag Attributes
|
||||
friendlyName: privatekey
|
||||
localKeyID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
Key Attributes: <No Attributes>
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
XXXXxyXXXXXXXxxyxxxX9y0XXYXXXXYXXxXyxxXxXxXXXyXXXXx4yx1xy1xyYxxY
|
||||
1XxYy38YxXxxxyXxyyxx+xxxxyx1Y1xYx7yx2/Y1XyyXYYYxY5YXxX0xY/Y642yX
|
||||
zYYxYXzXYxY0Y8y9YxyYXxxX40YyXxxXX4XXxx7XxXxxXyXxYYXxXyxX5XY0Yy2X
|
||||
1YX0XXyy6YXyXx9XxXxyXX9XXYXxXxXXXXXXxYXYY3Y8Yy311XYYY81XyY14Xyyx
|
||||
xXyx7xxXXXxxxxyyyX4YYYXyYyYXyxX4XYXYyxXYyx9xy23xXYyXyxYxXxx1XXXY
|
||||
y98yX6yYxyyyX4Xyx1Xy/0yxxYxXxYYx2xx7yYXXXxYXXXxyXyyYYxx5XX2xxyxy
|
||||
y6Yyyx0XX3YYYyx9YYXXXX7y0yxXXy+90XYz1y2xyx7yXxX+8X0xYxXXYxxyxYYy
|
||||
YXx8Yy4yX0Xyxxx6yYX92yxy1YYYzyyyyxy55x/yyXXXYYXYXXzXXxYYxyXY8XXX
|
||||
+y9+yXxX7XxxyYYxxXYxyY623xxXxYX59x5Y6yYyXYY4YxXXYXXXYxXYxXxXXx6x
|
||||
YXX7XxXX2X0XY7YXyYy1XXxYXxXxYY1xXXxxxyy+07zXYxYxxXyyxxyxXx1XYy5X
|
||||
5XYzyxYxXXYyX9XX7xX8xXxx+XXYyYXXXX5YY1x8Yxyx54Xy/1XXyyYXY5YxYyxY
|
||||
XyyxXyX/YxxXXXxXXYXxyxx63xX/xxyYXXyYzx0XY+YxX5xyYyyxxxXXYX/94XXy
|
||||
Xx63xYxXyXY3/XXxyyXX15XXXyz08XYY5YYXY/YXy/96x68XyyXXxYyXy4xYXx5x
|
||||
7yxxyxxYxXxyx3y=
|
||||
-----END PRIVATE KEY-----
|
|
@ -1,114 +0,0 @@
|
|||
// Copyright 2011 The goauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This program makes a read only call to the Google Cloud Storage API,
|
||||
// authenticated with OAuth2. A list of example APIs can be found at
|
||||
// https://code.google.com/oauthplayground/
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/goauth2/oauth/jwt"
|
||||
)
|
||||
|
||||
const scope = "https://www.googleapis.com/auth/devstorage.read_only"
|
||||
|
||||
var (
|
||||
secretsFile = flag.String("s", "", "JSON encoded secrets for the service account")
|
||||
pemFile = flag.String("k", "", "private pem key file for the service account")
|
||||
)
|
||||
|
||||
const usageMsg = `
|
||||
You must specify -k and -s.
|
||||
|
||||
To obtain client secrets and pem, see the "OAuth 2 Credentials" section under
|
||||
the "API Access" tab on this page: https://code.google.com/apis/console/
|
||||
|
||||
Google Cloud Storage must also be turned on in the API console.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *secretsFile == "" || *pemFile == "" {
|
||||
flag.Usage()
|
||||
fmt.Println(usageMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the secret file bytes into the config.
|
||||
secretBytes, err := ioutil.ReadFile(*secretsFile)
|
||||
if err != nil {
|
||||
log.Fatal("error reading secerets file:", err)
|
||||
}
|
||||
var config struct {
|
||||
Web struct {
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientID string `json:"client_id"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
}
|
||||
err = json.Unmarshal(secretBytes, &config)
|
||||
if err != nil {
|
||||
log.Fatal("error unmarshalling secerets:", err)
|
||||
}
|
||||
|
||||
// Get the project ID from the client ID.
|
||||
projectID := strings.SplitN(config.Web.ClientID, "-", 2)[0]
|
||||
|
||||
// Read the pem file bytes for the private key.
|
||||
keyBytes, err := ioutil.ReadFile(*pemFile)
|
||||
if err != nil {
|
||||
log.Fatal("error reading private key file:", err)
|
||||
}
|
||||
|
||||
// Craft the ClaimSet and JWT token.
|
||||
t := jwt.NewToken(config.Web.ClientEmail, scope, keyBytes)
|
||||
t.ClaimSet.Aud = config.Web.TokenURI
|
||||
|
||||
// We need to provide a client.
|
||||
c := &http.Client{}
|
||||
|
||||
// Get the access token.
|
||||
o, err := t.Assert(c)
|
||||
if err != nil {
|
||||
log.Fatal("assertion error:", err)
|
||||
}
|
||||
|
||||
// Refresh token will be missing, but this access_token will be good
|
||||
// for one hour.
|
||||
fmt.Printf("access_token = %v\n", o.AccessToken)
|
||||
fmt.Printf("refresh_token = %v\n", o.RefreshToken)
|
||||
fmt.Printf("expires %v\n", o.Expiry)
|
||||
|
||||
// Form the request to list Google Cloud Storage buckets.
|
||||
req, err := http.NewRequest("GET", "https://storage.googleapis.com/", nil)
|
||||
if err != nil {
|
||||
log.Fatal("http.NewRequest:", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "OAuth "+o.AccessToken)
|
||||
req.Header.Set("x-goog-api-version", "2")
|
||||
req.Header.Set("x-goog-project-id", projectID)
|
||||
|
||||
// Make the request.
|
||||
r, err := c.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal("API request error:", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Write the response to standard output.
|
||||
res, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Fatal("error reading API request results:", err)
|
||||
}
|
||||
fmt.Printf("\nRESULT:\n%s\n", res)
|
||||
}
|
|
@ -1,511 +0,0 @@
|
|||
// Copyright 2012 The goauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The jwt package provides support for creating credentials for OAuth2 service
|
||||
// account requests.
|
||||
//
|
||||
// For examples of the package usage please see jwt_test.go.
|
||||
// Example usage (error handling omitted for brevity):
|
||||
//
|
||||
// // Craft the ClaimSet and JWT token.
|
||||
// iss := "XXXXXXXXXXXX@developer.gserviceaccount.com"
|
||||
// scope := "https://www.googleapis.com/auth/devstorage.read_only"
|
||||
// t := jwt.NewToken(iss, scope, pemKeyBytes)
|
||||
//
|
||||
// // We need to provide a client.
|
||||
// c := &http.Client{}
|
||||
//
|
||||
// // Get the access token.
|
||||
// o, _ := t.Assert(c)
|
||||
//
|
||||
// // Form the request to the service.
|
||||
// req, _ := http.NewRequest("GET", "https://storage.googleapis.com/", nil)
|
||||
// req.Header.Set("Authorization", "OAuth "+o.AccessToken)
|
||||
// req.Header.Set("x-goog-api-version", "2")
|
||||
// req.Header.Set("x-goog-project-id", "XXXXXXXXXXXX")
|
||||
//
|
||||
// // Make the request.
|
||||
// result, _ := c.Do(req)
|
||||
//
|
||||
// For info on OAuth2 service accounts please see the online documentation.
|
||||
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount
|
||||
//
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/goauth2/oauth"
|
||||
)
|
||||
|
||||
// These are the default/standard values for this to work for Google service accounts.
|
||||
const (
|
||||
stdAlgorithm = "RS256"
|
||||
stdType = "JWT"
|
||||
stdAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer"
|
||||
stdGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
||||
stdAud = "https://accounts.google.com/o/oauth2/token"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidKey = errors.New("Invalid Key")
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Aud is usually https://accounts.google.com/o/oauth2/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).
|
||||
Prn string `json:"prn,omitempty"` // email for which the application is requesting delegated access (Optional).
|
||||
Exp int64 `json:"exp"`
|
||||
Iat int64 `json:"iat"`
|
||||
Typ string `json:"typ,omitempty"`
|
||||
Sub string `json:"sub,omitempty"` // Add support for googleapi delegation support
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// setTimes sets iat and exp to time.Now() and iat.Add(time.Hour) respectively.
|
||||
//
|
||||
// Note that these times have nothing to do with the expiration time for the
|
||||
// access_token returned by the server. These have to do with the lifetime of
|
||||
// the encoded JWT.
|
||||
//
|
||||
// A JWT can be re-used for up to one hour after it was encoded. The access
|
||||
// token that is granted will also be good for one hour so there is little point
|
||||
// in trying to use the JWT a second time.
|
||||
func (c *ClaimSet) setTimes(t time.Time) {
|
||||
c.iat = t
|
||||
c.exp = c.iat.Add(time.Hour)
|
||||
}
|
||||
|
||||
var (
|
||||
jsonStart = []byte{'{'}
|
||||
jsonEnd = []byte{'}'}
|
||||
)
|
||||
|
||||
// encode returns the Base64url encoded form of the Signature.
|
||||
func (c *ClaimSet) encode() string {
|
||||
if c.exp.IsZero() || c.iat.IsZero() {
|
||||
c.setTimes(time.Now())
|
||||
}
|
||||
if c.Aud == "" {
|
||||
c.Aud = stdAud
|
||||
}
|
||||
c.Exp = c.exp.Unix()
|
||||
c.Iat = c.iat.Unix()
|
||||
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(c.PrivateClaims) == 0 {
|
||||
return base64Encode(b)
|
||||
}
|
||||
|
||||
// Marshal private claim set and then append it to b.
|
||||
prv, err := json.Marshal(c.PrivateClaims)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Invalid map of private claims %v", c.PrivateClaims))
|
||||
}
|
||||
|
||||
// Concatenate public and private claim JSON objects.
|
||||
if !bytes.HasSuffix(b, jsonEnd) {
|
||||
panic(fmt.Errorf("Invalid JSON %s", b))
|
||||
}
|
||||
if !bytes.HasPrefix(prv, jsonStart) {
|
||||
panic(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)
|
||||
}
|
||||
|
||||
// Header describes the algorithm and type of token being generated,
|
||||
// and optionally a KeyID describing additional parameters for the
|
||||
// signature.
|
||||
type Header struct {
|
||||
Algorithm string `json:"alg"`
|
||||
Type string `json:"typ"`
|
||||
KeyId string `json:"kid,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Header) encode() string {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64Encode(b)
|
||||
}
|
||||
|
||||
// A JWT is composed of three parts: a header, a claim set, and a signature.
|
||||
// The well formed and encoded JWT can then be exchanged for an access token.
|
||||
//
|
||||
// The Token is not a JWT, but is is encoded to produce a well formed JWT.
|
||||
//
|
||||
// When obtaining a key from the Google API console it will be downloaded in a
|
||||
// PKCS12 encoding. To use this key you will need to convert it to a PEM file.
|
||||
// This can be achieved with openssl.
|
||||
//
|
||||
// $ openssl pkcs12 -in <key.p12> -nocerts -passin pass:notasecret -nodes -out <key.pem>
|
||||
//
|
||||
// The contents of this file can then be used as the Key.
|
||||
type Token struct {
|
||||
ClaimSet *ClaimSet // claim set used to construct the JWT
|
||||
Header *Header // header used to construct the JWT
|
||||
Key []byte // PEM printable encoding of the private key
|
||||
pKey *rsa.PrivateKey
|
||||
|
||||
header string
|
||||
claim string
|
||||
sig string
|
||||
|
||||
useExternalSigner bool
|
||||
signer Signer
|
||||
}
|
||||
|
||||
// NewToken returns a filled in *Token based on the standard header,
|
||||
// and sets the Iat and Exp times based on when the call to Assert is
|
||||
// made.
|
||||
func NewToken(iss, scope string, key []byte) *Token {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
Aud: stdAud,
|
||||
}
|
||||
h := &Header{
|
||||
Algorithm: stdAlgorithm,
|
||||
Type: stdType,
|
||||
}
|
||||
t := &Token{
|
||||
ClaimSet: c,
|
||||
Header: h,
|
||||
Key: key,
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// Signer is an interface that given a JWT token, returns the header &
|
||||
// claim (serialized and urlEncoded to a byte slice), along with the
|
||||
// signature and an error (if any occured). It could modify any data
|
||||
// to sign (typically the KeyID).
|
||||
//
|
||||
// Example usage where a SHA256 hash of the original url-encoded token
|
||||
// with an added KeyID and secret data is used as a signature:
|
||||
//
|
||||
// var privateData = "secret data added to hash, indexed by KeyID"
|
||||
//
|
||||
// type SigningService struct{}
|
||||
//
|
||||
// func (ss *SigningService) Sign(in *jwt.Token) (newTokenData, sig []byte, err error) {
|
||||
// in.Header.KeyID = "signing service"
|
||||
// newTokenData = in.EncodeWithoutSignature()
|
||||
// dataToSign := fmt.Sprintf("%s.%s", newTokenData, privateData)
|
||||
// h := sha256.New()
|
||||
// _, err := h.Write([]byte(dataToSign))
|
||||
// sig = h.Sum(nil)
|
||||
// return
|
||||
// }
|
||||
type Signer interface {
|
||||
Sign(in *Token) (tokenData, signature []byte, err error)
|
||||
}
|
||||
|
||||
// NewSignerToken returns a *Token, using an external signer function
|
||||
func NewSignerToken(iss, scope string, signer Signer) *Token {
|
||||
t := NewToken(iss, scope, nil)
|
||||
t.useExternalSigner = true
|
||||
t.signer = signer
|
||||
return t
|
||||
}
|
||||
|
||||
// Expired returns a boolean value letting us know if the token has expired.
|
||||
func (t *Token) Expired() bool {
|
||||
return t.ClaimSet.exp.Before(time.Now())
|
||||
}
|
||||
|
||||
// Encode constructs and signs a Token returning a JWT ready to use for
|
||||
// requesting an access token.
|
||||
func (t *Token) Encode() (string, error) {
|
||||
var tok string
|
||||
t.header = t.Header.encode()
|
||||
t.claim = t.ClaimSet.encode()
|
||||
err := t.sign()
|
||||
if err != nil {
|
||||
return tok, err
|
||||
}
|
||||
tok = fmt.Sprintf("%s.%s.%s", t.header, t.claim, t.sig)
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// EncodeWithoutSignature returns the url-encoded value of the Token
|
||||
// before signing has occured (typically for use by external signers).
|
||||
func (t *Token) EncodeWithoutSignature() string {
|
||||
t.header = t.Header.encode()
|
||||
t.claim = t.ClaimSet.encode()
|
||||
return fmt.Sprintf("%s.%s", t.header, t.claim)
|
||||
}
|
||||
|
||||
// sign computes the signature for a Token. The details for this can be found
|
||||
// in the OAuth2 Service Account documentation.
|
||||
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature
|
||||
func (t *Token) sign() error {
|
||||
if t.useExternalSigner {
|
||||
fulldata, sig, err := t.signer.Sign(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
split := strings.Split(string(fulldata), ".")
|
||||
if len(split) != 2 {
|
||||
return errors.New("no token returned")
|
||||
}
|
||||
t.header = split[0]
|
||||
t.claim = split[1]
|
||||
t.sig = base64Encode(sig)
|
||||
return err
|
||||
}
|
||||
ss := fmt.Sprintf("%s.%s", t.header, t.claim)
|
||||
if t.pKey == nil {
|
||||
err := t.parsePrivateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
h := sha256.New()
|
||||
h.Write([]byte(ss))
|
||||
b, err := rsa.SignPKCS1v15(rand.Reader, t.pKey, crypto.SHA256, h.Sum(nil))
|
||||
t.sig = base64Encode(b)
|
||||
return err
|
||||
}
|
||||
|
||||
// parsePrivateKey converts the Token's Key ([]byte) into a parsed
|
||||
// rsa.PrivateKey. If the key is not well formed this method will return an
|
||||
// ErrInvalidKey error.
|
||||
func (t *Token) parsePrivateKey() error {
|
||||
block, _ := pem.Decode(t.Key)
|
||||
if block == nil {
|
||||
return ErrInvalidKey
|
||||
}
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var ok bool
|
||||
t.pKey, ok = parsedKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return ErrInvalidKey
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Assert obtains an *oauth.Token from the remote server by encoding and sending
|
||||
// a JWT. The access_token will expire in one hour (3600 seconds) and cannot be
|
||||
// refreshed (no refresh_token is returned with the response). Once this token
|
||||
// expires call this method again to get a fresh one.
|
||||
func (t *Token) Assert(c *http.Client) (*oauth.Token, error) {
|
||||
var o *oauth.Token
|
||||
t.ClaimSet.setTimes(time.Now())
|
||||
u, v, err := t.buildRequest()
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
resp, err := c.PostForm(u, v)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o, err = handleResponse(resp)
|
||||
return o, err
|
||||
}
|
||||
|
||||
// buildRequest sets up the URL values and the proper URL string for making our
|
||||
// access_token request.
|
||||
func (t *Token) buildRequest() (string, url.Values, error) {
|
||||
v := url.Values{}
|
||||
j, err := t.Encode()
|
||||
if err != nil {
|
||||
return t.ClaimSet.Aud, v, err
|
||||
}
|
||||
v.Set("grant_type", stdGrantType)
|
||||
v.Set("assertion", j)
|
||||
return t.ClaimSet.Aud, v, nil
|
||||
}
|
||||
|
||||
// Used for decoding the response body.
|
||||
type respBody struct {
|
||||
IdToken string `json:"id_token"`
|
||||
Access string `json:"access_token"`
|
||||
Type string `json:"token_type"`
|
||||
ExpiresIn time.Duration `json:"expires_in"`
|
||||
}
|
||||
|
||||
// handleResponse returns a filled in *oauth.Token given the *http.Response from
|
||||
// a *http.Request created by buildRequest.
|
||||
func handleResponse(r *http.Response) (*oauth.Token, error) {
|
||||
o := &oauth.Token{}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode != 200 {
|
||||
return o, errors.New("invalid response: " + r.Status)
|
||||
}
|
||||
b := &respBody{}
|
||||
err := json.NewDecoder(r.Body).Decode(b)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o.AccessToken = b.Access
|
||||
if b.IdToken != "" {
|
||||
// decode returned id token to get expiry
|
||||
o.AccessToken = b.IdToken
|
||||
s := strings.Split(b.IdToken, ".")
|
||||
if len(s) < 2 {
|
||||
return nil, errors.New("invalid token received")
|
||||
}
|
||||
d, err := base64Decode(s[1])
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
c := &ClaimSet{}
|
||||
err = json.NewDecoder(bytes.NewBuffer(d)).Decode(c)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
o.Expiry = time.Unix(c.Exp, 0)
|
||||
return o, nil
|
||||
}
|
||||
o.Expiry = time.Now().Add(b.ExpiresIn * time.Second)
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Transport implements http.RoundTripper. When configured with a valid
|
||||
// JWT and OAuth tokens it can be used to make authenticated HTTP requests.
|
||||
//
|
||||
// t := &jwt.Transport{jwtToken, oauthToken}
|
||||
// r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
|
||||
//
|
||||
// It will automatically refresh the OAuth token if it can, updating in place.
|
||||
type Transport struct {
|
||||
JWTToken *Token
|
||||
OAuthToken *oauth.Token
|
||||
|
||||
// Transport is the HTTP transport to use when making requests.
|
||||
// It will default to http.DefaultTransport if nil.
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// Creates a new authenticated transport.
|
||||
func NewTransport(token *Token) (*Transport, error) {
|
||||
oa, err := token.Assert(new(http.Client))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Transport{
|
||||
JWTToken: token,
|
||||
OAuthToken: oa,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Client returns an *http.Client that makes OAuth-authenticated requests.
|
||||
func (t *Transport) Client() *http.Client {
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
// Fetches the internal transport.
|
||||
func (t *Transport) transport() http.RoundTripper {
|
||||
if t.Transport != nil {
|
||||
return t.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// RoundTrip executes a single HTTP transaction using the Transport's
|
||||
// OAuthToken as authorization headers.
|
||||
//
|
||||
// This method will attempt to renew the token if it has expired and may return
|
||||
// an error related to that token renewal before attempting the client request.
|
||||
// If the token cannot be renewed a non-nil os.Error value will be returned.
|
||||
// If the token is invalid callers should expect HTTP-level errors,
|
||||
// as indicated by the Response's StatusCode.
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Sanity check the two tokens
|
||||
if t.JWTToken == nil {
|
||||
return nil, fmt.Errorf("no JWT token supplied")
|
||||
}
|
||||
if t.OAuthToken == nil {
|
||||
return nil, fmt.Errorf("no OAuth token supplied")
|
||||
}
|
||||
// Refresh the OAuth token if it has expired
|
||||
if t.OAuthToken.Expired() {
|
||||
if oa, err := t.JWTToken.Assert(new(http.Client)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
t.OAuthToken = oa
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
req.Header.Set("Authorization", "Bearer "+t.OAuthToken.AccessToken)
|
||||
|
||||
// Make the HTTP request.
|
||||
return t.transport().RoundTrip(req)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -1,486 +0,0 @@
|
|||
// Copyright 2012 The goauth2 Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// For package documentation please see jwt.go.
|
||||
//
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
stdHeaderStr = `{"alg":"RS256","typ":"JWT"}`
|
||||
iss = "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com"
|
||||
scope = "https://www.googleapis.com/auth/prediction"
|
||||
exp = 1328554385
|
||||
iat = 1328550785 // exp + 1 hour
|
||||
)
|
||||
|
||||
// Base64url encoded Header
|
||||
const headerEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
|
||||
// Base64url encoded ClaimSet
|
||||
const claimSetEnc = "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ"
|
||||
|
||||
// Base64url encoded Signature
|
||||
const sigEnc = "olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
|
||||
|
||||
// Base64url encoded Token
|
||||
const tokEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
|
||||
|
||||
// Private key for testing
|
||||
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
|
||||
7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
|
||||
xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
|
||||
SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
|
||||
pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
|
||||
SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
|
||||
nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
|
||||
HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
|
||||
nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
|
||||
IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
|
||||
YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
|
||||
Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
|
||||
vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
|
||||
B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
|
||||
aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
|
||||
eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
|
||||
aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
|
||||
klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
|
||||
CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
|
||||
UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
|
||||
soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
|
||||
bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
|
||||
504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
|
||||
YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
|
||||
BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
// Public key to go with the private key for testing
|
||||
const publicKeyPem = `-----BEGIN CERTIFICATE-----
|
||||
MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
|
||||
BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
|
||||
MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
|
||||
7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
|
||||
uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
|
||||
gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
|
||||
+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
|
||||
ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
|
||||
gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
|
||||
GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
|
||||
AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
|
||||
odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
|
||||
+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
|
||||
ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
|
||||
ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
|
||||
cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
var (
|
||||
privateKeyPemBytes = []byte(privateKeyPem)
|
||||
publicKeyPemBytes = []byte(publicKeyPem)
|
||||
stdHeader = &Header{Algorithm: stdAlgorithm, Type: stdType}
|
||||
)
|
||||
|
||||
// Testing the urlEncode function.
|
||||
func TestUrlEncode(t *testing.T) {
|
||||
enc := base64Encode([]byte(stdHeaderStr))
|
||||
b := []byte(enc)
|
||||
if b[len(b)-1] == 61 {
|
||||
t.Error("TestUrlEncode: last chat == \"=\"")
|
||||
}
|
||||
if enc != headerEnc {
|
||||
t.Error("TestUrlEncode: enc != headerEnc")
|
||||
t.Errorf(" enc = %s", enc)
|
||||
t.Errorf(" headerEnc = %s", headerEnc)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the times are set properly.
|
||||
func TestClaimSetSetTimes(t *testing.T) {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
}
|
||||
iat := time.Unix(iat, 0)
|
||||
c.setTimes(iat)
|
||||
if c.exp.Unix() != exp {
|
||||
t.Error("TestClaimSetSetTimes: c.exp != exp")
|
||||
t.Errorf(" c.Exp = %d", c.exp.Unix())
|
||||
t.Errorf(" exp = %d", exp)
|
||||
}
|
||||
}
|
||||
|
||||
// Given a well formed ClaimSet, test for proper encoding.
|
||||
func TestClaimSetEncode(t *testing.T) {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
exp: time.Unix(exp, 0),
|
||||
iat: time.Unix(iat, 0),
|
||||
}
|
||||
enc := c.encode()
|
||||
re, err := base64Decode(enc)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding encoded claim set: %v", err)
|
||||
}
|
||||
|
||||
wa, err := base64Decode(claimSetEnc)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding encoded expected claim set: %v", err)
|
||||
}
|
||||
|
||||
if enc != claimSetEnc {
|
||||
t.Error("TestClaimSetEncode: enc != claimSetEnc")
|
||||
t.Errorf(" enc = %s", string(re))
|
||||
t.Errorf(" claimSetEnc = %s", string(wa))
|
||||
}
|
||||
}
|
||||
|
||||
// Test that claim sets with private claim names are encoded correctly.
|
||||
func TestClaimSetWithPrivateNameEncode(t *testing.T) {
|
||||
iatT := time.Unix(iat, 0)
|
||||
expT := time.Unix(exp, 0)
|
||||
|
||||
i, err := json.Marshal(iatT.Unix())
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling iatT value of %v: %v", iatT.Unix(), err)
|
||||
}
|
||||
iatStr := string(i)
|
||||
e, err := json.Marshal(expT.Unix())
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling expT value of %v: %v", expT.Unix(), err)
|
||||
}
|
||||
|
||||
expStr := string(e)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input map[string]interface{}
|
||||
want string
|
||||
}{
|
||||
// Test a simple int field.
|
||||
{
|
||||
"single simple field",
|
||||
map[string]interface{}{"amount": 22},
|
||||
`{` +
|
||||
`"iss":"` + iss + `",` +
|
||||
`"scope":"` + scope + `",` +
|
||||
`"aud":"` + stdAud + `",` +
|
||||
`"exp":` + expStr + `,` +
|
||||
`"iat":` + iatStr + `,` +
|
||||
`"amount":22` +
|
||||
`}`,
|
||||
},
|
||||
{
|
||||
"multiple simple fields",
|
||||
map[string]interface{}{"tracking_code": "axZf", "amount": 22},
|
||||
`{` +
|
||||
`"iss":"` + iss + `",` +
|
||||
`"scope":"` + scope + `",` +
|
||||
`"aud":"` + stdAud + `",` +
|
||||
`"exp":` + expStr + `,` +
|
||||
`"iat":` + iatStr + `,` +
|
||||
`"amount":22,` +
|
||||
`"tracking_code":"axZf"` +
|
||||
`}`,
|
||||
},
|
||||
{
|
||||
"nested struct fields",
|
||||
map[string]interface{}{
|
||||
"tracking_code": "axZf",
|
||||
"purchase": struct {
|
||||
Description string `json:"desc"`
|
||||
Quantity int32 `json:"q"`
|
||||
Time int64 `json:"t"`
|
||||
}{
|
||||
"toaster",
|
||||
5,
|
||||
iat,
|
||||
},
|
||||
},
|
||||
`{` +
|
||||
`"iss":"` + iss + `",` +
|
||||
`"scope":"` + scope + `",` +
|
||||
`"aud":"` + stdAud + `",` +
|
||||
`"exp":` + expStr + `,` +
|
||||
`"iat":` + iatStr + `,` +
|
||||
`"purchase":{"desc":"toaster","q":5,"t":` + iatStr + `},` +
|
||||
`"tracking_code":"axZf"` +
|
||||
`}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
Aud: stdAud,
|
||||
iat: iatT,
|
||||
exp: expT,
|
||||
PrivateClaims: testCase.input,
|
||||
}
|
||||
cJSON, err := base64Decode(c.encode())
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding claim set: %v", err)
|
||||
}
|
||||
if string(cJSON) != testCase.want {
|
||||
t.Errorf("TestClaimSetWithPrivateNameEncode: enc != want in case %s", testCase.desc)
|
||||
t.Errorf(" enc = %s", cJSON)
|
||||
t.Errorf(" want = %s", testCase.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test the NewToken constructor.
|
||||
func TestNewToken(t *testing.T) {
|
||||
tok := NewToken(iss, scope, privateKeyPemBytes)
|
||||
if tok.ClaimSet.Iss != iss {
|
||||
t.Error("TestNewToken: tok.ClaimSet.Iss != iss")
|
||||
t.Errorf(" tok.ClaimSet.Iss = %s", tok.ClaimSet.Iss)
|
||||
t.Errorf(" iss = %s", iss)
|
||||
}
|
||||
if tok.ClaimSet.Scope != scope {
|
||||
t.Error("TestNewToken: tok.ClaimSet.Scope != scope")
|
||||
t.Errorf(" tok.ClaimSet.Scope = %s", tok.ClaimSet.Scope)
|
||||
t.Errorf(" scope = %s", scope)
|
||||
}
|
||||
if tok.ClaimSet.Aud != stdAud {
|
||||
t.Error("TestNewToken: tok.ClaimSet.Aud != stdAud")
|
||||
t.Errorf(" tok.ClaimSet.Aud = %s", tok.ClaimSet.Aud)
|
||||
t.Errorf(" stdAud = %s", stdAud)
|
||||
}
|
||||
if !bytes.Equal(tok.Key, privateKeyPemBytes) {
|
||||
t.Error("TestNewToken: tok.Key != privateKeyPemBytes")
|
||||
t.Errorf(" tok.Key = %s", tok.Key)
|
||||
t.Errorf(" privateKeyPemBytes = %s", privateKeyPemBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the private key parsing functions work.
|
||||
func TestParsePrivateKey(t *testing.T) {
|
||||
tok := &Token{
|
||||
Key: privateKeyPemBytes,
|
||||
}
|
||||
err := tok.parsePrivateKey()
|
||||
if err != nil {
|
||||
t.Errorf("TestParsePrivateKey:tok.parsePrivateKey: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the token signature generated matches the golden standard.
|
||||
func TestTokenSign(t *testing.T) {
|
||||
tok := &Token{
|
||||
Key: privateKeyPemBytes,
|
||||
claim: claimSetEnc,
|
||||
header: headerEnc,
|
||||
}
|
||||
err := tok.parsePrivateKey()
|
||||
if err != nil {
|
||||
t.Errorf("TestTokenSign:tok.parsePrivateKey: %v", err)
|
||||
}
|
||||
err = tok.sign()
|
||||
if err != nil {
|
||||
t.Errorf("TestTokenSign:tok.sign: %v", err)
|
||||
}
|
||||
if tok.sig != sigEnc {
|
||||
t.Error("TestTokenSign: tok.sig != sigEnc")
|
||||
t.Errorf(" tok.sig = %s", tok.sig)
|
||||
t.Errorf(" sigEnc = %s", sigEnc)
|
||||
}
|
||||
}
|
||||
|
||||
// Test that the token expiration function is working.
|
||||
func TestTokenExpired(t *testing.T) {
|
||||
c := &ClaimSet{}
|
||||
tok := &Token{
|
||||
ClaimSet: c,
|
||||
}
|
||||
now := time.Now()
|
||||
c.setTimes(now)
|
||||
if tok.Expired() != false {
|
||||
t.Error("TestTokenExpired: tok.Expired != false")
|
||||
}
|
||||
// Set the times as if they were set 2 hours ago.
|
||||
c.setTimes(now.Add(-2 * time.Hour))
|
||||
if tok.Expired() != true {
|
||||
t.Error("TestTokenExpired: tok.Expired != true")
|
||||
}
|
||||
}
|
||||
|
||||
// Given a well formed Token, test for proper encoding.
|
||||
func TestTokenEncode(t *testing.T) {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
exp: time.Unix(exp, 0),
|
||||
iat: time.Unix(iat, 0),
|
||||
}
|
||||
tok := &Token{
|
||||
ClaimSet: c,
|
||||
Header: stdHeader,
|
||||
Key: privateKeyPemBytes,
|
||||
}
|
||||
enc, err := tok.Encode()
|
||||
if err != nil {
|
||||
t.Errorf("TestTokenEncode:tok.Assertion: %v", err)
|
||||
}
|
||||
if enc != tokEnc {
|
||||
t.Error("TestTokenEncode: enc != tokEnc")
|
||||
t.Errorf(" enc = %s", enc)
|
||||
t.Errorf(" tokEnc = %s", tokEnc)
|
||||
}
|
||||
}
|
||||
|
||||
// Given a well formed Token we should get back a well formed request.
|
||||
func TestBuildRequest(t *testing.T) {
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
exp: time.Unix(exp, 0),
|
||||
iat: time.Unix(iat, 0),
|
||||
}
|
||||
tok := &Token{
|
||||
ClaimSet: c,
|
||||
Header: stdHeader,
|
||||
Key: privateKeyPemBytes,
|
||||
}
|
||||
u, v, err := tok.buildRequest()
|
||||
if err != nil {
|
||||
t.Errorf("TestBuildRequest:BuildRequest: %v", err)
|
||||
}
|
||||
if u != c.Aud {
|
||||
t.Error("TestBuildRequest: u != c.Aud")
|
||||
t.Errorf(" u = %s", u)
|
||||
t.Errorf(" c.Aud = %s", c.Aud)
|
||||
}
|
||||
if v.Get("grant_type") != stdGrantType {
|
||||
t.Error("TestBuildRequest: grant_type != stdGrantType")
|
||||
t.Errorf(" grant_type = %s", v.Get("grant_type"))
|
||||
t.Errorf(" stdGrantType = %s", stdGrantType)
|
||||
}
|
||||
if v.Get("assertion") != tokEnc {
|
||||
t.Error("TestBuildRequest: assertion != tokEnc")
|
||||
t.Errorf(" assertion = %s", v.Get("assertion"))
|
||||
t.Errorf(" tokEnc = %s", tokEnc)
|
||||
}
|
||||
}
|
||||
|
||||
// Given a well formed access request response we should get back a oauth.Token.
|
||||
func TestHandleResponse(t *testing.T) {
|
||||
rb := &respBody{
|
||||
Access: "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
|
||||
Type: "Bearer",
|
||||
ExpiresIn: 3600,
|
||||
}
|
||||
b, err := json.Marshal(rb)
|
||||
if err != nil {
|
||||
t.Errorf("TestHandleResponse:json.Marshal: %v", err)
|
||||
}
|
||||
r := &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}
|
||||
o, err := handleResponse(r)
|
||||
if err != nil {
|
||||
t.Errorf("TestHandleResponse:handleResponse: %v", err)
|
||||
}
|
||||
if o.AccessToken != rb.Access {
|
||||
t.Error("TestHandleResponse: o.AccessToken != rb.Access")
|
||||
t.Errorf(" o.AccessToken = %s", o.AccessToken)
|
||||
t.Errorf(" rb.Access = %s", rb.Access)
|
||||
}
|
||||
if o.Expired() {
|
||||
t.Error("TestHandleResponse: o.Expired == true")
|
||||
}
|
||||
}
|
||||
|
||||
// passthrough signature for test
|
||||
type FakeSigner struct{}
|
||||
|
||||
func (f FakeSigner) Sign(tok *Token) ([]byte, []byte, error) {
|
||||
block, _ := pem.Decode(privateKeyPemBytes)
|
||||
pKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
ss := headerEnc + "." + claimSetEnc
|
||||
h := sha256.New()
|
||||
h.Write([]byte(ss))
|
||||
b, _ := rsa.SignPKCS1v15(rand.Reader, pKey, crypto.SHA256, h.Sum(nil))
|
||||
return []byte(ss), b, nil
|
||||
}
|
||||
|
||||
// Given an external signer, get back a valid and signed JWT
|
||||
func TestExternalSigner(t *testing.T) {
|
||||
tok := NewSignerToken(iss, scope, FakeSigner{})
|
||||
enc, _ := tok.Encode()
|
||||
if enc != tokEnc {
|
||||
t.Errorf("TestExternalSigner: enc != tokEnc")
|
||||
t.Errorf(" enc = %s", enc)
|
||||
t.Errorf(" tokEnc = %s", tokEnc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleResponseWithNewExpiry(t *testing.T) {
|
||||
rb := &respBody{
|
||||
IdToken: tokEnc,
|
||||
}
|
||||
b, err := json.Marshal(rb)
|
||||
if err != nil {
|
||||
t.Errorf("TestHandleResponse:json.Marshal: %v", err)
|
||||
}
|
||||
r := &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}
|
||||
o, err := handleResponse(r)
|
||||
if err != nil {
|
||||
t.Errorf("TestHandleResponse:handleResponse: %v", err)
|
||||
}
|
||||
if o.Expiry != time.Unix(exp, 0) {
|
||||
t.Error("TestHandleResponse: o.Expiry != exp")
|
||||
t.Errorf(" o.Expiry = %s", o.Expiry)
|
||||
t.Errorf(" exp = %s", time.Unix(exp, 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder for future Assert tests.
|
||||
func TestAssert(t *testing.T) {
|
||||
// Since this method makes a call to BuildRequest, an htttp.Client, and
|
||||
// finally HandleResponse there is not much more to test. This is here
|
||||
// as a placeholder if that changes.
|
||||
}
|
||||
|
||||
// Benchmark for the end-to-end encoding of a well formed token.
|
||||
func BenchmarkTokenEncode(b *testing.B) {
|
||||
b.StopTimer()
|
||||
c := &ClaimSet{
|
||||
Iss: iss,
|
||||
Scope: scope,
|
||||
exp: time.Unix(exp, 0),
|
||||
iat: time.Unix(iat, 0),
|
||||
}
|
||||
tok := &Token{
|
||||
ClaimSet: c,
|
||||
Key: privateKeyPemBytes,
|
||||
}
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
tok.Encode()
|
||||
}
|
||||
}
|
|
@ -1,476 +0,0 @@
|
|||
// Copyright 2011 The goauth2 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 oauth supports making OAuth2-authenticated HTTP requests.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Specify your configuration. (typically as a global variable)
|
||||
// var config = &oauth.Config{
|
||||
// ClientId: YOUR_CLIENT_ID,
|
||||
// ClientSecret: YOUR_CLIENT_SECRET,
|
||||
// Scope: "https://www.googleapis.com/auth/buzz",
|
||||
// AuthURL: "https://accounts.google.com/o/oauth2/auth",
|
||||
// TokenURL: "https://accounts.google.com/o/oauth2/token",
|
||||
// RedirectURL: "http://you.example.org/handler",
|
||||
// }
|
||||
//
|
||||
// // 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 := &oauth.Transport{Config: config}
|
||||
// t.Exchange(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(...)
|
||||
// // ...
|
||||
// // btw, r.FormValue("state") == "foo"
|
||||
// }
|
||||
//
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OAuthError is the error type returned by many operations.
|
||||
//
|
||||
// In retrospect it should not exist. Don't depend on it.
|
||||
type OAuthError struct {
|
||||
prefix string
|
||||
msg string
|
||||
}
|
||||
|
||||
func (oe OAuthError) Error() string {
|
||||
return "OAuthError: " + oe.prefix + ": " + oe.msg
|
||||
}
|
||||
|
||||
// Cache specifies the methods that implement a Token cache.
|
||||
type Cache interface {
|
||||
Token() (*Token, error)
|
||||
PutToken(*Token) error
|
||||
}
|
||||
|
||||
// CacheFile implements Cache. Its value is the name of the file in which
|
||||
// the Token is stored in JSON format.
|
||||
type CacheFile string
|
||||
|
||||
func (f CacheFile) Token() (*Token, error) {
|
||||
file, err := os.Open(string(f))
|
||||
if err != nil {
|
||||
return nil, OAuthError{"CacheFile.Token", err.Error()}
|
||||
}
|
||||
defer file.Close()
|
||||
tok := &Token{}
|
||||
if err := json.NewDecoder(file).Decode(tok); err != nil {
|
||||
return nil, OAuthError{"CacheFile.Token", err.Error()}
|
||||
}
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
func (f CacheFile) PutToken(tok *Token) error {
|
||||
file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return OAuthError{"CacheFile.PutToken", err.Error()}
|
||||
}
|
||||
if err := json.NewEncoder(file).Encode(tok); err != nil {
|
||||
file.Close()
|
||||
return OAuthError{"CacheFile.PutToken", err.Error()}
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return OAuthError{"CacheFile.PutToken", err.Error()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config is the configuration of an OAuth consumer.
|
||||
type Config struct {
|
||||
// ClientId is the OAuth client identifier used when communicating with
|
||||
// the configured OAuth provider.
|
||||
ClientId string
|
||||
|
||||
// ClientSecret is the OAuth client secret used when communicating with
|
||||
// the configured OAuth provider.
|
||||
ClientSecret string
|
||||
|
||||
// Scope identifies the level of access being requested. Multiple scope
|
||||
// values should be provided as a space-delimited string.
|
||||
Scope string
|
||||
|
||||
// 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
|
||||
|
||||
// RedirectURL is the URL to which the user will be returned after
|
||||
// granting (or denying) access.
|
||||
RedirectURL string
|
||||
|
||||
// TokenCache allows tokens to be cached for subsequent requests.
|
||||
TokenCache Cache
|
||||
|
||||
// AccessType is an OAuth extension that gets sent as the
|
||||
// "access_type" field in the URL from AuthCodeURL.
|
||||
// See https://developers.google.com/accounts/docs/OAuth2WebServer.
|
||||
// It may be "online" (the default) or "offline".
|
||||
// If your application needs to refresh access tokens when the
|
||||
// user is not present at the browser, then use offline. This
|
||||
// will result in your application obtaining a refresh token
|
||||
// the first time your application exchanges an authorization
|
||||
// code for a user.
|
||||
AccessType string
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Token contains an end-user's tokens.
|
||||
// This is the data you must store to persist authentication.
|
||||
type Token struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
Expiry time.Time // If zero the token has no (known) expiry time.
|
||||
|
||||
// Extra optionally contains extra metadata from the server
|
||||
// when updating a token. The only current key that may be
|
||||
// populated is "id_token". It may be nil and will be
|
||||
// initialized as needed.
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
// Expired reports whether the token has expired or is invalid.
|
||||
func (t *Token) Expired() bool {
|
||||
if t.AccessToken == "" {
|
||||
return true
|
||||
}
|
||||
if t.Expiry.IsZero() {
|
||||
return false
|
||||
}
|
||||
return t.Expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
// Transport implements http.RoundTripper. When configured with a valid
|
||||
// Config and Token it can be used to make authenticated HTTP requests.
|
||||
//
|
||||
// t := &oauth.Transport{config}
|
||||
// t.Exchange(code)
|
||||
// // t now contains a valid Token
|
||||
// r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
|
||||
//
|
||||
// It will automatically refresh the Token if it can,
|
||||
// updating the supplied Token in place.
|
||||
type Transport struct {
|
||||
*Config
|
||||
*Token
|
||||
|
||||
// mu guards modifying the token.
|
||||
mu sync.Mutex
|
||||
|
||||
// Transport is the HTTP transport to use when making requests.
|
||||
// It will default to http.DefaultTransport if nil.
|
||||
// (It should never be an oauth.Transport.)
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// Client returns an *http.Client that makes OAuth-authenticated requests.
|
||||
func (t *Transport) Client() *http.Client {
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
func (t *Transport) transport() http.RoundTripper {
|
||||
if t.Transport != nil {
|
||||
return t.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// AuthCodeURL returns a URL that the end-user should be redirected to,
|
||||
// so that they may obtain an authorization code.
|
||||
func (c *Config) AuthCodeURL(state string) string {
|
||||
url_, err := url.Parse(c.AuthURL)
|
||||
if err != nil {
|
||||
panic("AuthURL malformed: " + err.Error())
|
||||
}
|
||||
q := url.Values{
|
||||
"response_type": {"code"},
|
||||
"client_id": {c.ClientId},
|
||||
"state": condVal(state),
|
||||
"scope": condVal(c.Scope),
|
||||
"redirect_uri": condVal(c.RedirectURL),
|
||||
"access_type": condVal(c.AccessType),
|
||||
"approval_prompt": condVal(c.ApprovalPrompt),
|
||||
}.Encode()
|
||||
if url_.RawQuery == "" {
|
||||
url_.RawQuery = q
|
||||
} else {
|
||||
url_.RawQuery += "&" + q
|
||||
}
|
||||
return url_.String()
|
||||
}
|
||||
|
||||
func condVal(v string) []string {
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
// Exchange takes a code and gets access Token from the remote server.
|
||||
func (t *Transport) Exchange(code string) (*Token, error) {
|
||||
if t.Config == nil {
|
||||
return nil, OAuthError{"Exchange", "no Config supplied"}
|
||||
}
|
||||
|
||||
// If the transport or the cache already has a token, it is
|
||||
// passed to `updateToken` to preserve existing refresh token.
|
||||
tok := t.Token
|
||||
if tok == nil && t.TokenCache != nil {
|
||||
tok, _ = t.TokenCache.Token()
|
||||
}
|
||||
if tok == nil {
|
||||
tok = new(Token)
|
||||
}
|
||||
err := t.updateToken(tok, url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"redirect_uri": {t.RedirectURL},
|
||||
"scope": {t.Scope},
|
||||
"code": {code},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Token = tok
|
||||
if t.TokenCache != nil {
|
||||
return tok, t.TokenCache.PutToken(tok)
|
||||
}
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
// RoundTrip executes a single HTTP transaction using the Transport's
|
||||
// Token as authorization headers.
|
||||
//
|
||||
// This method will attempt to renew the Token if it has expired and may return
|
||||
// an error related to that Token renewal before attempting the client request.
|
||||
// If the Token cannot be renewed a non-nil os.Error value will be returned.
|
||||
// If the Token is invalid callers should expect HTTP-level errors,
|
||||
// as indicated by the Response's StatusCode.
|
||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
accessToken, err := t.getAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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)
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
// Make the HTTP request.
|
||||
return t.transport().RoundTrip(req)
|
||||
}
|
||||
|
||||
func (t *Transport) getAccessToken() (string, error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.Token == nil {
|
||||
if t.Config == nil {
|
||||
return "", OAuthError{"RoundTrip", "no Config supplied"}
|
||||
}
|
||||
if t.TokenCache == nil {
|
||||
return "", OAuthError{"RoundTrip", "no Token supplied"}
|
||||
}
|
||||
var err error
|
||||
t.Token, err = t.TokenCache.Token()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh the Token if it has expired.
|
||||
if t.Expired() {
|
||||
if err := t.Refresh(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if t.AccessToken == "" {
|
||||
return "", errors.New("no access token obtained from refresh")
|
||||
}
|
||||
return t.AccessToken, 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
|
||||
}
|
||||
|
||||
// Refresh renews the Transport's AccessToken using its RefreshToken.
|
||||
func (t *Transport) Refresh() error {
|
||||
if t.Token == nil {
|
||||
return OAuthError{"Refresh", "no existing Token"}
|
||||
}
|
||||
if t.RefreshToken == "" {
|
||||
return OAuthError{"Refresh", "Token expired; no Refresh Token"}
|
||||
}
|
||||
if t.Config == nil {
|
||||
return OAuthError{"Refresh", "no Config supplied"}
|
||||
}
|
||||
|
||||
err := t.updateToken(t.Token, url.Values{
|
||||
"grant_type": {"refresh_token"},
|
||||
"refresh_token": {t.RefreshToken},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.TokenCache != nil {
|
||||
return t.TokenCache.PutToken(t.Token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthenticateClient gets an access Token using the client_credentials grant
|
||||
// type.
|
||||
func (t *Transport) AuthenticateClient() error {
|
||||
if t.Config == nil {
|
||||
return OAuthError{"Exchange", "no Config supplied"}
|
||||
}
|
||||
if t.Token == nil {
|
||||
t.Token = &Token{}
|
||||
}
|
||||
return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}})
|
||||
}
|
||||
|
||||
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
|
||||
// implements the OAuth2 spec correctly
|
||||
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
|
||||
// In summary:
|
||||
// - Reddit only accepts client secret in the Authorization header
|
||||
// - Dropbox accepts either it in URL param or Auth header, but not both.
|
||||
// - Google only accepts URL param (not spec compliant?), not Auth header
|
||||
func providerAuthHeaderWorks(tokenURL string) bool {
|
||||
if strings.HasPrefix(tokenURL, "https://accounts.google.com/") ||
|
||||
strings.HasPrefix(tokenURL, "https://github.com/") ||
|
||||
strings.HasPrefix(tokenURL, "https://api.instagram.com/") ||
|
||||
strings.HasPrefix(tokenURL, "https://www.douban.com/") {
|
||||
// Some sites fail to implement the OAuth2 spec fully.
|
||||
return false
|
||||
}
|
||||
|
||||
// Assume the provider implements the spec properly
|
||||
// otherwise. We can add more exceptions as they're
|
||||
// discovered. We will _not_ be adding configurable hooks
|
||||
// to this package to let users select server bugs.
|
||||
return true
|
||||
}
|
||||
|
||||
// updateToken mutates both tok and v.
|
||||
func (t *Transport) updateToken(tok *Token, v url.Values) error {
|
||||
v.Set("client_id", t.ClientId)
|
||||
bustedAuth := !providerAuthHeaderWorks(t.TokenURL)
|
||||
if bustedAuth {
|
||||
v.Set("client_secret", t.ClientSecret)
|
||||
}
|
||||
client := &http.Client{Transport: t.transport()}
|
||||
req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if !bustedAuth {
|
||||
req.SetBasicAuth(t.ClientId, t.ClientSecret)
|
||||
}
|
||||
r, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Body.Close()
|
||||
if r.StatusCode != 200 {
|
||||
return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status}
|
||||
}
|
||||
var b struct {
|
||||
Access string `json:"access_token"`
|
||||
Refresh string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"` // seconds
|
||||
Id string `json:"id_token"`
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
switch content {
|
||||
case "application/x-www-form-urlencoded", "text/plain":
|
||||
vals, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.Access = vals.Get("access_token")
|
||||
b.Refresh = vals.Get("refresh_token")
|
||||
b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64)
|
||||
b.Id = vals.Get("id_token")
|
||||
default:
|
||||
if err = json.Unmarshal(body, &b); err != nil {
|
||||
return fmt.Errorf("got bad response from server: %q", body)
|
||||
}
|
||||
}
|
||||
if b.Access == "" {
|
||||
return errors.New("received empty access token from authorization server")
|
||||
}
|
||||
tok.AccessToken = b.Access
|
||||
// Don't overwrite `RefreshToken` with an empty value
|
||||
if b.Refresh != "" {
|
||||
tok.RefreshToken = b.Refresh
|
||||
}
|
||||
if b.ExpiresIn == 0 {
|
||||
tok.Expiry = time.Time{}
|
||||
} else {
|
||||
tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second)
|
||||
}
|
||||
if b.Id != "" {
|
||||
if tok.Extra == nil {
|
||||
tok.Extra = make(map[string]string)
|
||||
}
|
||||
tok.Extra["id_token"] = b.Id
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
// Copyright 2011 The goauth2 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 oauth
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var requests = []struct {
|
||||
path, query, auth string // request
|
||||
contenttype, body string // response
|
||||
}{
|
||||
{
|
||||
path: "/token",
|
||||
query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d",
|
||||
contenttype: "application/json",
|
||||
auth: "Basic Y2wxM250MWQ6czNjcjN0",
|
||||
body: `
|
||||
{
|
||||
"access_token":"token1",
|
||||
"refresh_token":"refreshtoken1",
|
||||
"id_token":"idtoken1",
|
||||
"expires_in":3600
|
||||
}
|
||||
`,
|
||||
},
|
||||
{path: "/secure", auth: "Bearer token1", body: "first payload"},
|
||||
{
|
||||
path: "/token",
|
||||
query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d",
|
||||
contenttype: "application/json",
|
||||
auth: "Basic Y2wxM250MWQ6czNjcjN0",
|
||||
body: `
|
||||
{
|
||||
"access_token":"token2",
|
||||
"refresh_token":"refreshtoken2",
|
||||
"id_token":"idtoken2",
|
||||
"expires_in":3600
|
||||
}
|
||||
`,
|
||||
},
|
||||
{path: "/secure", auth: "Bearer token2", body: "second payload"},
|
||||
{
|
||||
path: "/token",
|
||||
query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d",
|
||||
contenttype: "application/x-www-form-urlencoded",
|
||||
body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600",
|
||||
auth: "Basic Y2wxM250MWQ6czNjcjN0",
|
||||
},
|
||||
{path: "/secure", auth: "Bearer token3", body: "third payload"},
|
||||
{
|
||||
path: "/token",
|
||||
query: "grant_type=client_credentials&client_id=cl13nt1d",
|
||||
contenttype: "application/json",
|
||||
auth: "Basic Y2wxM250MWQ6czNjcjN0",
|
||||
body: `
|
||||
{
|
||||
"access_token":"token4",
|
||||
"expires_in":3600
|
||||
}
|
||||
`,
|
||||
},
|
||||
{path: "/secure", auth: "Bearer token4", body: "fourth payload"},
|
||||
}
|
||||
|
||||
func TestOAuth(t *testing.T) {
|
||||
// Set up test server.
|
||||
n := 0
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
if n >= len(requests) {
|
||||
t.Errorf("too many requests: %d", n)
|
||||
return
|
||||
}
|
||||
req := requests[n]
|
||||
n++
|
||||
|
||||
// Check request.
|
||||
if g, w := r.URL.Path, req.path; g != w {
|
||||
t.Errorf("request[%d] got path %s, want %s", n, g, w)
|
||||
}
|
||||
want, _ := url.ParseQuery(req.query)
|
||||
for k := range want {
|
||||
if g, w := r.FormValue(k), want.Get(k); g != w {
|
||||
t.Errorf("query[%s] = %s, want %s", k, g, w)
|
||||
}
|
||||
}
|
||||
if g, w := r.Header.Get("Authorization"), req.auth; w != "" && g != w {
|
||||
t.Errorf("Authorization: %v, want %v", g, w)
|
||||
}
|
||||
|
||||
// Send response.
|
||||
w.Header().Set("Content-Type", req.contenttype)
|
||||
io.WriteString(w, req.body)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer server.Close()
|
||||
|
||||
config := &Config{
|
||||
ClientId: "cl13nt1d",
|
||||
ClientSecret: "s3cr3t",
|
||||
Scope: "https://example.net/scope",
|
||||
AuthURL: server.URL + "/auth",
|
||||
TokenURL: server.URL + "/token",
|
||||
}
|
||||
|
||||
// TODO(adg): test AuthCodeURL
|
||||
|
||||
transport := &Transport{Config: config}
|
||||
_, err := transport.Exchange("c0d3")
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange: %v", err)
|
||||
}
|
||||
checkToken(t, transport.Token, "token1", "refreshtoken1", "idtoken1")
|
||||
|
||||
c := transport.Client()
|
||||
resp, err := c.Get(server.URL + "/secure")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
checkBody(t, resp, "first payload")
|
||||
|
||||
// test automatic refresh
|
||||
transport.Expiry = time.Now().Add(-time.Hour)
|
||||
resp, err = c.Get(server.URL + "/secure")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
checkBody(t, resp, "second payload")
|
||||
checkToken(t, transport.Token, "token2", "refreshtoken2", "idtoken2")
|
||||
|
||||
// refresh one more time, but get URL-encoded token instead of JSON
|
||||
transport.Expiry = time.Now().Add(-time.Hour)
|
||||
resp, err = c.Get(server.URL + "/secure")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
checkBody(t, resp, "third payload")
|
||||
checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3")
|
||||
|
||||
transport.Token = &Token{}
|
||||
err = transport.AuthenticateClient()
|
||||
if err != nil {
|
||||
t.Fatalf("AuthenticateClient: %v", err)
|
||||
}
|
||||
checkToken(t, transport.Token, "token4", "", "")
|
||||
resp, err = c.Get(server.URL + "/secure")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
checkBody(t, resp, "fourth payload")
|
||||
}
|
||||
|
||||
func checkToken(t *testing.T, tok *Token, access, refresh, id string) {
|
||||
if g, w := tok.AccessToken, access; g != w {
|
||||
t.Errorf("AccessToken = %q, want %q", g, w)
|
||||
}
|
||||
if g, w := tok.RefreshToken, refresh; g != w {
|
||||
t.Errorf("RefreshToken = %q, want %q", g, w)
|
||||
}
|
||||
if g, w := tok.Extra["id_token"], id; g != w {
|
||||
t.Errorf("Extra['id_token'] = %q, want %q", g, w)
|
||||
}
|
||||
if tok.Expiry.IsZero() {
|
||||
t.Errorf("Expiry is zero; want ~1 hour")
|
||||
} else {
|
||||
exp := tok.Expiry.Sub(time.Now())
|
||||
const slop = 3 * time.Second // time moving during test
|
||||
if (time.Hour-slop) > exp || exp > time.Hour {
|
||||
t.Errorf("Expiry = %v, want ~1 hour", exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkBody(t *testing.T, r *http.Response, body string) {
|
||||
b, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("reading reponse body: %v, want %q", err, body)
|
||||
}
|
||||
if g, w := string(b), body; g != w {
|
||||
t.Errorf("request body mismatch: got %q, want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCachePermissions(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't support file mode bits.
|
||||
return
|
||||
}
|
||||
|
||||
td, err := ioutil.TempDir("", "oauth-test")
|
||||
if err != nil {
|
||||
t.Fatalf("ioutil.TempDir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
tempFile := filepath.Join(td, "cache-file")
|
||||
|
||||
cf := CacheFile(tempFile)
|
||||
if err := cf.PutToken(new(Token)); err != nil {
|
||||
t.Fatalf("PutToken: %v", err)
|
||||
}
|
||||
fi, err := os.Stat(tempFile)
|
||||
if err != nil {
|
||||
t.Fatalf("os.Stat: %v", err)
|
||||
}
|
||||
if fi.Mode()&0077 != 0 {
|
||||
t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenExpired(t *testing.T) {
|
||||
tests := []struct {
|
||||
token Token
|
||||
expired bool
|
||||
}{
|
||||
{Token{AccessToken: "foo"}, false},
|
||||
{Token{AccessToken: ""}, true},
|
||||
{Token{AccessToken: "foo", Expiry: time.Now().Add(-1 * time.Hour)}, true},
|
||||
{Token{AccessToken: "foo", Expiry: time.Now().Add(1 * time.Hour)}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.token.Expired(); got != tt.expired {
|
||||
t.Errorf("token %+v Expired = %v; want %v", tt.token, got, !got)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue