oauth2, oauth2/jwt: break JWT off into its own package

Change-Id: Iaaa36728f87744e0d9609674f0d0ad96e6ac80b4
Reviewed-on: https://go-review.googlesource.com/2198
Reviewed-by: Burcu Dogan <jbd@google.com>
This commit is contained in:
Brad Fitzpatrick 2014-12-30 15:11:02 -08:00
parent dfb470cc49
commit ed997606a9
8 changed files with 87 additions and 49 deletions

View File

@ -48,24 +48,3 @@ func ExampleConfig() {
client := conf.Client(oauth2.NoContext, tok) client := conf.Client(oauth2.NoContext, tok)
client.Get("...") client.Get("...")
} }
func ExampleJWTConfig() {
conf := &oauth2.JWTConfig{
Email: "xxx@developer.com",
// The contents of your RSA private key or your PEM file
// that contains a private key.
// 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
//
// It only supports PEM containers with no passphrase.
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
Subject: "user@example.com",
TokenURL: "https://provider.com/o/oauth2/token",
}
// Initiate an http.Client, the following GET request will be
// authorized and authenticated on the behalf of user@example.com.
client := conf.Client(oauth2.NoContext)
client.Get("...")
}

View File

@ -15,6 +15,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google" "golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"google.golang.org/appengine" "google.golang.org/appengine"
"google.golang.org/appengine/urlfetch" "google.golang.org/appengine/urlfetch"
) )
@ -76,7 +77,7 @@ func ExampleJWTConfigFromJSON() {
func Example_serviceAccount() { func Example_serviceAccount() {
// Your credentials should be obtained from the Google // Your credentials should be obtained from the Google
// Developer Console (https://console.developers.google.com). // Developer Console (https://console.developers.google.com).
conf := &oauth2.JWTConfig{ conf := &jwt.Config{
Email: "xxx@developer.gserviceaccount.com", Email: "xxx@developer.gserviceaccount.com",
// The contents of your RSA private key or your PEM file // The contents of your RSA private key or your PEM file
// that contains a private key. // that contains a private key.

View File

@ -21,6 +21,7 @@ import (
"time" "time"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
) )
// TODO(bradfitz,jbd): import "google.golang.org/cloud/compute/metadata" instead of // TODO(bradfitz,jbd): import "google.golang.org/cloud/compute/metadata" instead of
@ -39,7 +40,7 @@ const JWTTokenURL = "https://accounts.google.com/o/oauth2/token"
// the credentials that authorize and authenticate the requests. // the credentials that authorize and authenticate the requests.
// Create a service account on "Credentials" page under "APIs & Auth" for your // Create a service account on "Credentials" page under "APIs & Auth" for your
// project at https://console.developers.google.com to download a JSON key file. // project at https://console.developers.google.com to download a JSON key file.
func JWTConfigFromJSON(ctx oauth2.Context, jsonKey []byte, scope ...string) (*oauth2.JWTConfig, error) { func JWTConfigFromJSON(ctx oauth2.Context, jsonKey []byte, scope ...string) (*jwt.Config, error) {
var key struct { var key struct {
Email string `json:"client_email"` Email string `json:"client_email"`
PrivateKey string `json:"private_key"` PrivateKey string `json:"private_key"`
@ -47,7 +48,7 @@ func JWTConfigFromJSON(ctx oauth2.Context, jsonKey []byte, scope ...string) (*oa
if err := json.Unmarshal(jsonKey, &key); err != nil { if err := json.Unmarshal(jsonKey, &key); err != nil {
return nil, err return nil, err
} }
return &oauth2.JWTConfig{ return &jwt.Config{
Email: key.Email, Email: key.Email,
PrivateKey: []byte(key.PrivateKey), PrivateKey: []byte(key.PrivateKey),
Scopes: scope, Scopes: scope,

31
jwt/example_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2014 The oauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jwt_test
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
)
func ExampleJWTConfig() {
conf := &jwt.Config{
Email: "xxx@developer.com",
// The contents of your RSA private key or your PEM file
// that contains a private key.
// 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
//
// It only supports PEM containers with no passphrase.
PrivateKey: []byte("-----BEGIN RSA PRIVATE KEY-----..."),
Subject: "user@example.com",
TokenURL: "https://provider.com/o/oauth2/token",
}
// Initiate an http.Client, the following GET request will be
// authorized and authenticated on the behalf of user@example.com.
client := conf.Client(oauth2.NoContext)
client.Get("...")
}

View File

@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package oauth2 // Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly
// known as "two-legged OAuth 2.0".
//
// See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
package jwt
import ( import (
"encoding/json" "encoding/json"
@ -14,6 +18,7 @@ import (
"strings" "strings"
"time" "time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/internal" "golang.org/x/oauth2/internal"
"golang.org/x/oauth2/jws" "golang.org/x/oauth2/jws"
) )
@ -23,9 +28,9 @@ var (
defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"}
) )
// JWTConfig is the configuration for using JWT to fetch tokens, // Config is the configuration for using JWT to fetch tokens,
// commonly known as "two-legged OAuth". // commonly known as "two-legged OAuth 2.0".
type JWTConfig struct { type Config struct {
// Email is the OAuth client identifier used when communicating with // Email is the OAuth client identifier used when communicating with
// the configured OAuth provider. // the configured OAuth provider.
Email string Email string
@ -52,8 +57,8 @@ type JWTConfig struct {
// TokenSource returns a JWT TokenSource using the configuration // TokenSource returns a JWT TokenSource using the configuration
// in c and the HTTP client from the provided context. // in c and the HTTP client from the provided context.
func (c *JWTConfig) TokenSource(ctx Context) TokenSource { func (c *Config) TokenSource(ctx oauth2.Context) oauth2.TokenSource {
return ReuseTokenSource(nil, jwtSource{ctx, c}) return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
} }
// Client returns an HTTP client wrapping the context's // Client returns an HTTP client wrapping the context's
@ -61,26 +66,23 @@ func (c *JWTConfig) TokenSource(ctx Context) TokenSource {
// obtained from c. // obtained from c.
// //
// The returned client and its Transport should not be modified. // The returned client and its Transport should not be modified.
func (c *JWTConfig) Client(ctx Context) *http.Client { func (c *Config) Client(ctx oauth2.Context) *http.Client {
return NewClient(ctx, c.TokenSource(ctx)) return oauth2.NewClient(ctx, c.TokenSource(ctx))
} }
// jwtSource is a source that always does a signed JWT request for a token. // jwtSource is a source that always does a signed JWT request for a token.
// It should typically be wrapped with a reuseTokenSource. // It should typically be wrapped with a reuseTokenSource.
type jwtSource struct { type jwtSource struct {
ctx Context ctx oauth2.Context
conf *JWTConfig conf *Config
} }
func (js jwtSource) Token() (*Token, error) { func (js jwtSource) Token() (*oauth2.Token, error) {
pk, err := internal.ParseKey(js.conf.PrivateKey) pk, err := internal.ParseKey(js.conf.PrivateKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hc, err := contextClient(js.ctx) hc := oauth2.NewClient(js.ctx, nil)
if err != nil {
return nil, err
}
claimSet := &jws.ClaimSet{ claimSet := &jws.ClaimSet{
Iss: js.conf.Email, Iss: js.conf.Email,
Scope: strings.Join(js.conf.Scopes, " "), Scope: strings.Join(js.conf.Scopes, " "),
@ -121,12 +123,13 @@ func (js jwtSource) Token() (*Token, error) {
if err := json.Unmarshal(body, &tokenRes); err != nil { if err := json.Unmarshal(body, &tokenRes); err != nil {
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
} }
token := &Token{ token := &oauth2.Token{
AccessToken: tokenRes.AccessToken, AccessToken: tokenRes.AccessToken,
TokenType: tokenRes.TokenType, TokenType: tokenRes.TokenType,
raw: make(map[string]interface{}),
} }
json.Unmarshal(body, &token.raw) // no error checks for optional fields raw := make(map[string]interface{})
json.Unmarshal(body, &raw) // no error checks for optional fields
token = token.WithExtra(raw)
if secs := tokenRes.ExpiresIn; secs > 0 { if secs := tokenRes.ExpiresIn; secs > 0 {
token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)

View File

@ -2,12 +2,14 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package oauth2 package jwt
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"golang.org/x/oauth2"
) )
var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
@ -50,12 +52,12 @@ func TestJWTFetch_JSONResponse(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
conf := &JWTConfig{ conf := &Config{
Email: "aaa@xxx.com", Email: "aaa@xxx.com",
PrivateKey: dummyPrivateKey, PrivateKey: dummyPrivateKey,
TokenURL: ts.URL, TokenURL: ts.URL,
} }
tok, err := conf.TokenSource(NoContext).Token() tok, err := conf.TokenSource(oauth2.NoContext).Token()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -84,12 +86,12 @@ func TestJWTFetch_BadResponse(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
conf := &JWTConfig{ conf := &Config{
Email: "aaa@xxx.com", Email: "aaa@xxx.com",
PrivateKey: dummyPrivateKey, PrivateKey: dummyPrivateKey,
TokenURL: ts.URL, TokenURL: ts.URL,
} }
tok, err := conf.TokenSource(NoContext).Token() tok, err := conf.TokenSource(oauth2.NoContext).Token()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -117,12 +119,12 @@ func TestJWTFetch_BadResponseType(t *testing.T) {
w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`))
})) }))
defer ts.Close() defer ts.Close()
conf := &JWTConfig{ conf := &Config{
Email: "aaa@xxx.com", Email: "aaa@xxx.com",
PrivateKey: dummyPrivateKey, PrivateKey: dummyPrivateKey,
TokenURL: ts.URL, TokenURL: ts.URL,
} }
tok, err := conf.TokenSource(NoContext).Token() tok, err := conf.TokenSource(oauth2.NoContext).Token()
if err == nil { if err == nil {
t.Error("got a token; expected error") t.Error("got a token; expected error")
if tok.AccessToken != "" { if tok.AccessToken != "" {

View File

@ -412,7 +412,18 @@ type contextKey struct{}
// NewClient creates an *http.Client from a Context and TokenSource. // NewClient creates an *http.Client from a Context and TokenSource.
// The returned client is not valid beyond the lifetime of the context. // The returned client is not valid beyond the lifetime of the context.
//
// As a special case, if src is nil, a non-OAuth2 client is returned
// using the provided context. This exists to support related OAuth2
// packages.
func NewClient(ctx Context, src TokenSource) *http.Client { func NewClient(ctx Context, src TokenSource) *http.Client {
if src == nil {
c, err := contextClient(ctx)
if err != nil {
return &http.Client{Transport: errorTransport{err}}
}
return c
}
return &http.Client{ return &http.Client{
Transport: &Transport{ Transport: &Transport{
Base: contextTransport(ctx), Base: contextTransport(ctx),

View File

@ -60,6 +60,16 @@ func (t *Token) SetAuthHeader(r *http.Request) {
r.Header.Set("Authorization", t.Type()+" "+t.AccessToken) r.Header.Set("Authorization", t.Type()+" "+t.AccessToken)
} }
// WithExtra returns a new Token that's a clone of t, but using the
// provided raw extra map. This is only intended for use by packages
// implementing derivative OAuth2 flows.
func (t *Token) WithExtra(extra interface{}) *Token {
t2 := new(Token)
*t2 = *t
t2.raw = extra
return t2
}
// Extra returns an extra field returned from the server during token // Extra returns an extra field returned from the server during token
// retrieval. // retrieval.
func (t *Token) Extra(key string) string { func (t *Token) Extra(key string) string {