From ed997606a947169f714c32991d0b090bf5d4ef12 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 30 Dec 2014 15:11:02 -0800 Subject: [PATCH] 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 --- example_test.go | 21 ------------------ google/example_test.go | 3 ++- google/google.go | 5 +++-- jwt/example_test.go | 31 +++++++++++++++++++++++++++ jwt.go => jwt/jwt.go | 39 ++++++++++++++++++---------------- jwt_test.go => jwt/jwt_test.go | 16 ++++++++------ oauth2.go | 11 ++++++++++ token.go | 10 +++++++++ 8 files changed, 87 insertions(+), 49 deletions(-) create mode 100644 jwt/example_test.go rename jwt.go => jwt/jwt.go (80%) rename jwt_test.go => jwt/jwt_test.go (94%) diff --git a/example_test.go b/example_test.go index d26a3dc..e4fef7d 100644 --- a/example_test.go +++ b/example_test.go @@ -48,24 +48,3 @@ func ExampleConfig() { client := conf.Client(oauth2.NoContext, tok) 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("...") -} diff --git a/google/example_test.go b/google/example_test.go index a59cfe9..ac61db2 100644 --- a/google/example_test.go +++ b/google/example_test.go @@ -15,6 +15,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" "google.golang.org/appengine" "google.golang.org/appengine/urlfetch" ) @@ -76,7 +77,7 @@ func ExampleJWTConfigFromJSON() { func Example_serviceAccount() { // Your credentials should be obtained from the Google // Developer Console (https://console.developers.google.com). - conf := &oauth2.JWTConfig{ + conf := &jwt.Config{ Email: "xxx@developer.gserviceaccount.com", // The contents of your RSA private key or your PEM file // that contains a private key. diff --git a/google/google.go b/google/google.go index eb6c92a..5916ceb 100644 --- a/google/google.go +++ b/google/google.go @@ -21,6 +21,7 @@ import ( "time" "golang.org/x/oauth2" + "golang.org/x/oauth2/jwt" ) // 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. // 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. -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 { Email string `json:"client_email"` 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 { return nil, err } - return &oauth2.JWTConfig{ + return &jwt.Config{ Email: key.Email, PrivateKey: []byte(key.PrivateKey), Scopes: scope, diff --git a/jwt/example_test.go b/jwt/example_test.go new file mode 100644 index 0000000..6d61883 --- /dev/null +++ b/jwt/example_test.go @@ -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("...") +} diff --git a/jwt.go b/jwt/jwt.go similarity index 80% rename from jwt.go rename to jwt/jwt.go index 9507671..a8e2138 100644 --- a/jwt.go +++ b/jwt/jwt.go @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style // 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 ( "encoding/json" @@ -14,6 +18,7 @@ import ( "strings" "time" + "golang.org/x/oauth2" "golang.org/x/oauth2/internal" "golang.org/x/oauth2/jws" ) @@ -23,9 +28,9 @@ var ( defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"} ) -// JWTConfig is the configuration for using JWT to fetch tokens, -// commonly known as "two-legged OAuth". -type JWTConfig struct { +// Config is the configuration for using JWT to fetch tokens, +// commonly known as "two-legged OAuth 2.0". +type Config struct { // Email is the OAuth client identifier used when communicating with // the configured OAuth provider. Email string @@ -52,8 +57,8 @@ type JWTConfig struct { // TokenSource returns a JWT TokenSource using the configuration // in c and the HTTP client from the provided context. -func (c *JWTConfig) TokenSource(ctx Context) TokenSource { - return ReuseTokenSource(nil, jwtSource{ctx, c}) +func (c *Config) TokenSource(ctx oauth2.Context) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c}) } // Client returns an HTTP client wrapping the context's @@ -61,26 +66,23 @@ func (c *JWTConfig) TokenSource(ctx Context) TokenSource { // obtained from c. // // The returned client and its Transport should not be modified. -func (c *JWTConfig) Client(ctx Context) *http.Client { - return NewClient(ctx, c.TokenSource(ctx)) +func (c *Config) Client(ctx oauth2.Context) *http.Client { + return oauth2.NewClient(ctx, c.TokenSource(ctx)) } // jwtSource is a source that always does a signed JWT request for a token. // It should typically be wrapped with a reuseTokenSource. type jwtSource struct { - ctx Context - conf *JWTConfig + ctx oauth2.Context + conf *Config } -func (js jwtSource) Token() (*Token, error) { +func (js jwtSource) Token() (*oauth2.Token, error) { pk, err := internal.ParseKey(js.conf.PrivateKey) if err != nil { return nil, err } - hc, err := contextClient(js.ctx) - if err != nil { - return nil, err - } + hc := oauth2.NewClient(js.ctx, nil) claimSet := &jws.ClaimSet{ Iss: js.conf.Email, Scope: strings.Join(js.conf.Scopes, " "), @@ -121,12 +123,13 @@ func (js jwtSource) Token() (*Token, error) { if err := json.Unmarshal(body, &tokenRes); err != nil { return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) } - token := &Token{ + token := &oauth2.Token{ AccessToken: tokenRes.AccessToken, 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 { token.Expiry = time.Now().Add(time.Duration(secs) * time.Second) diff --git a/jwt_test.go b/jwt/jwt_test.go similarity index 94% rename from jwt_test.go rename to jwt/jwt_test.go index e9a732c..da922c3 100644 --- a/jwt_test.go +++ b/jwt/jwt_test.go @@ -2,12 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package oauth2 +package jwt import ( "net/http" "net/http/httptest" "testing" + + "golang.org/x/oauth2" ) var dummyPrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- @@ -50,12 +52,12 @@ func TestJWTFetch_JSONResponse(t *testing.T) { })) defer ts.Close() - conf := &JWTConfig{ + conf := &Config{ Email: "aaa@xxx.com", PrivateKey: dummyPrivateKey, TokenURL: ts.URL, } - tok, err := conf.TokenSource(NoContext).Token() + tok, err := conf.TokenSource(oauth2.NoContext).Token() if err != nil { t.Fatal(err) } @@ -84,12 +86,12 @@ func TestJWTFetch_BadResponse(t *testing.T) { })) defer ts.Close() - conf := &JWTConfig{ + conf := &Config{ Email: "aaa@xxx.com", PrivateKey: dummyPrivateKey, TokenURL: ts.URL, } - tok, err := conf.TokenSource(NoContext).Token() + tok, err := conf.TokenSource(oauth2.NoContext).Token() if err != nil { t.Fatal(err) } @@ -117,12 +119,12 @@ func TestJWTFetch_BadResponseType(t *testing.T) { w.Write([]byte(`{"access_token":123, "scope": "user", "token_type": "bearer"}`)) })) defer ts.Close() - conf := &JWTConfig{ + conf := &Config{ Email: "aaa@xxx.com", PrivateKey: dummyPrivateKey, TokenURL: ts.URL, } - tok, err := conf.TokenSource(NoContext).Token() + tok, err := conf.TokenSource(oauth2.NoContext).Token() if err == nil { t.Error("got a token; expected error") if tok.AccessToken != "" { diff --git a/oauth2.go b/oauth2.go index a38e6a8..90f983b 100644 --- a/oauth2.go +++ b/oauth2.go @@ -412,7 +412,18 @@ type contextKey struct{} // NewClient creates an *http.Client from a Context and TokenSource. // 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 { + if src == nil { + c, err := contextClient(ctx) + if err != nil { + return &http.Client{Transport: errorTransport{err}} + } + return c + } return &http.Client{ Transport: &Transport{ Base: contextTransport(ctx), diff --git a/token.go b/token.go index b8a2938..dc88b3d 100644 --- a/token.go +++ b/token.go @@ -60,6 +60,16 @@ func (t *Token) SetAuthHeader(r *http.Request) { 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 // retrieval. func (t *Token) Extra(key string) string {