forked from remote/oauth2
Introduce an option function type
- Reduce the duplicate code by merging the flows and determining the flow type by looking at the provided options. - Options as a function type allows us to validate an individual an option in its scope and makes it easier to compose the built-in options with the third-party ones.
This commit is contained in:
@@ -7,7 +7,6 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -19,6 +18,82 @@ import (
|
||||
"appengine/urlfetch"
|
||||
)
|
||||
|
||||
var (
|
||||
// memcacheGob enables mocking of the memcache.Gob calls for unit testing.
|
||||
memcacheGob memcacher = &aeMemcache{}
|
||||
|
||||
// accessTokenFunc enables mocking of the appengine.AccessToken call for unit testing.
|
||||
accessTokenFunc = appengine.AccessToken
|
||||
|
||||
// mu protects multiple threads from attempting to fetch a token at the same time.
|
||||
mu sync.Mutex
|
||||
|
||||
// tokens implements a local cache of tokens to prevent hitting quota limits for appengine.AccessToken calls.
|
||||
tokens map[string]*oauth2.Token
|
||||
)
|
||||
|
||||
// safetyMargin is used to avoid clock-skew problems.
|
||||
// 5 minutes is conservative because tokens are valid for 60 minutes.
|
||||
const safetyMargin = 5 * time.Minute
|
||||
|
||||
func init() {
|
||||
tokens = make(map[string]*oauth2.Token)
|
||||
}
|
||||
|
||||
// AppEngineContext requires an App Engine request context.
|
||||
func AppEngineContext(ctx appengine.Context) oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
opts.TokenFetcherFunc = makeAppEngineTokenFetcher(ctx, opts)
|
||||
opts.Transport = &urlfetch.Transport{Context: ctx}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// FetchToken fetches a new access token for the provided scopes.
|
||||
// Tokens are cached locally and also with Memcache so that the app can scale
|
||||
// without hitting quota limits by calling appengine.AccessToken too frequently.
|
||||
func makeAppEngineTokenFetcher(ctx appengine.Context, opts *oauth2.Options) func(*oauth2.Token) (*oauth2.Token, error) {
|
||||
return func(existing *oauth2.Token) (*oauth2.Token, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
key := ":" + strings.Join(opts.Scopes, "_")
|
||||
now := time.Now().Add(safetyMargin)
|
||||
if t, ok := tokens[key]; ok && !t.Expiry.Before(now) {
|
||||
return t, nil
|
||||
}
|
||||
delete(tokens, key)
|
||||
|
||||
// Attempt to get token from Memcache
|
||||
tok := new(oauth2.Token)
|
||||
_, err := memcacheGob.Get(ctx, key, tok)
|
||||
if err == nil && !tok.Expiry.Before(now) {
|
||||
tokens[key] = tok // Save token locally
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
token, expiry, err := accessTokenFunc(ctx, opts.Scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
AccessToken: token,
|
||||
Expiry: expiry,
|
||||
}
|
||||
tokens[key] = t
|
||||
// Also back up token in Memcache
|
||||
if err = memcacheGob.Set(ctx, &memcache.Item{
|
||||
Key: key,
|
||||
Value: []byte{},
|
||||
Object: *t,
|
||||
Expiration: expiry.Sub(now),
|
||||
}); err != nil {
|
||||
ctx.Errorf("unexpected memcache.Set error: %v", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
// aeMemcache wraps the needed Memcache functionality to make it easy to mock
|
||||
type aeMemcache struct{}
|
||||
|
||||
@@ -34,99 +109,3 @@ type memcacher interface {
|
||||
Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error)
|
||||
Set(c appengine.Context, item *memcache.Item) error
|
||||
}
|
||||
|
||||
// memcacheGob enables mocking of the memcache.Gob calls for unit testing.
|
||||
var memcacheGob memcacher = &aeMemcache{}
|
||||
|
||||
// accessTokenFunc enables mocking of the appengine.AccessToken call for unit testing.
|
||||
var accessTokenFunc = appengine.AccessToken
|
||||
|
||||
// safetyMargin is used to avoid clock-skew problems.
|
||||
// 5 minutes is conservative because tokens are valid for 60 minutes.
|
||||
const safetyMargin = 5 * time.Minute
|
||||
|
||||
// mu protects multiple threads from attempting to fetch a token at the same time.
|
||||
var mu sync.Mutex
|
||||
|
||||
// tokens implements a local cache of tokens to prevent hitting quota limits for appengine.AccessToken calls.
|
||||
var tokens map[string]*oauth2.Token
|
||||
|
||||
func init() {
|
||||
tokens = make(map[string]*oauth2.Token)
|
||||
}
|
||||
|
||||
// AppEngineConfig represents a configuration for an
|
||||
// App Engine application's Google service account.
|
||||
type AppEngineConfig struct {
|
||||
// Transport is the http.RoundTripper to be used
|
||||
// to construct new oauth2.Transport instances from
|
||||
// this configuration.
|
||||
Transport http.RoundTripper
|
||||
|
||||
context appengine.Context
|
||||
scopes []string
|
||||
}
|
||||
|
||||
// NewAppEngineConfig creates a new AppEngineConfig for the
|
||||
// provided auth scopes.
|
||||
func NewAppEngineConfig(context appengine.Context, scopes ...string) *AppEngineConfig {
|
||||
return &AppEngineConfig{
|
||||
context: context,
|
||||
scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransport returns a transport that authorizes
|
||||
// the requests with the application's service account.
|
||||
func (c *AppEngineConfig) NewTransport() *oauth2.Transport {
|
||||
return oauth2.NewTransport(c.transport(), c, nil)
|
||||
}
|
||||
|
||||
// FetchToken fetches a new access token for the provided scopes.
|
||||
// Tokens are cached locally and also with Memcache so that the app can scale
|
||||
// without hitting quota limits by calling appengine.AccessToken too frequently.
|
||||
func (c *AppEngineConfig) FetchToken(existing *oauth2.Token) (*oauth2.Token, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
key := ":" + strings.Join(c.scopes, "_")
|
||||
now := time.Now().Add(safetyMargin)
|
||||
if t, ok := tokens[key]; ok && !t.Expiry.Before(now) {
|
||||
return t, nil
|
||||
}
|
||||
delete(tokens, key)
|
||||
|
||||
// Attempt to get token from Memcache
|
||||
tok := new(oauth2.Token)
|
||||
_, err := memcacheGob.Get(c.context, key, tok)
|
||||
if err == nil && !tok.Expiry.Before(now) {
|
||||
tokens[key] = tok // Save token locally
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
token, expiry, err := accessTokenFunc(c.context, c.scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
AccessToken: token,
|
||||
Expiry: expiry,
|
||||
}
|
||||
tokens[key] = t
|
||||
// Also back up token in Memcache
|
||||
if err = memcacheGob.Set(c.context, &memcache.Item{
|
||||
Key: key,
|
||||
Value: []byte{},
|
||||
Object: *t,
|
||||
Expiration: expiry.Sub(now),
|
||||
}); err != nil {
|
||||
c.context.Errorf("unexpected memcache.Set error: %v", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *AppEngineConfig) transport() http.RoundTripper {
|
||||
if c.Transport != nil {
|
||||
return c.Transport
|
||||
}
|
||||
return &urlfetch.Transport{Context: c.context}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package google
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -72,11 +73,16 @@ func TestFetchTokenLocalCacheMiss(t *testing.T) {
|
||||
memcacheGob = m
|
||||
accessTokenCount = 0
|
||||
delete(tokens, testScopeKey) // clear local cache
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
tr := f.NewTransport()
|
||||
c := http.Client{Transport: tr}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -102,8 +108,16 @@ func TestFetchTokenLocalCacheHit(t *testing.T) {
|
||||
AccessToken: "mytoken",
|
||||
Expiry: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tr := f.NewTransport()
|
||||
c := http.Client{Transport: tr}
|
||||
c.Get("server")
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
}
|
||||
@@ -139,11 +153,16 @@ func TestFetchTokenMemcacheHit(t *testing.T) {
|
||||
Expiration: 1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -180,11 +199,15 @@ func TestFetchTokenLocalCacheExpired(t *testing.T) {
|
||||
Expiration: 1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -217,11 +240,15 @@ func TestFetchTokenMemcacheExpired(t *testing.T) {
|
||||
Expiration: -1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -17,6 +16,81 @@ import (
|
||||
"google.golang.org/appengine/memcache"
|
||||
)
|
||||
|
||||
var (
|
||||
// memcacheGob enables mocking of the memcache.Gob calls for unit testing.
|
||||
memcacheGob memcacher = &aeMemcache{}
|
||||
|
||||
// accessTokenFunc enables mocking of the appengine.AccessToken call for unit testing.
|
||||
accessTokenFunc = appengine.AccessToken
|
||||
|
||||
// mu protects multiple threads from attempting to fetch a token at the same time.
|
||||
mu sync.Mutex
|
||||
|
||||
// tokens implements a local cache of tokens to prevent hitting quota limits for appengine.AccessToken calls.
|
||||
tokens map[string]*oauth2.Token
|
||||
)
|
||||
|
||||
// safetyMargin is used to avoid clock-skew problems.
|
||||
// 5 minutes is conservative because tokens are valid for 60 minutes.
|
||||
const safetyMargin = 5 * time.Minute
|
||||
|
||||
func init() {
|
||||
tokens = make(map[string]*oauth2.Token)
|
||||
}
|
||||
|
||||
// AppEngineContext requires an App Engine request context.
|
||||
func AppEngineContext(ctx appengine.Context) oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
opts.TokenFetcherFunc = makeAppEngineTokenFetcher(ctx, opts)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// FetchToken fetches a new access token for the provided scopes.
|
||||
// Tokens are cached locally and also with Memcache so that the app can scale
|
||||
// without hitting quota limits by calling appengine.AccessToken too frequently.
|
||||
func makeAppEngineTokenFetcher(ctx appengine.Context, opts *oauth2.Options) func(*oauth2.Token) (*oauth2.Token, error) {
|
||||
return func(existing *oauth2.Token) (*oauth2.Token, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
key := ":" + strings.Join(opts.Scopes, "_")
|
||||
now := time.Now().Add(safetyMargin)
|
||||
if t, ok := tokens[key]; ok && !t.Expiry.Before(now) {
|
||||
return t, nil
|
||||
}
|
||||
delete(tokens, key)
|
||||
|
||||
// Attempt to get token from Memcache
|
||||
tok := new(oauth2.Token)
|
||||
_, err := memcacheGob.Get(ctx, key, tok)
|
||||
if err == nil && !tok.Expiry.Before(now) {
|
||||
tokens[key] = tok // Save token locally
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
token, expiry, err := accessTokenFunc(ctx, opts.Scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
AccessToken: token,
|
||||
Expiry: expiry,
|
||||
}
|
||||
tokens[key] = t
|
||||
// Also back up token in Memcache
|
||||
if err = memcacheGob.Set(ctx, &memcache.Item{
|
||||
Key: key,
|
||||
Value: []byte{},
|
||||
Object: *t,
|
||||
Expiration: expiry.Sub(now),
|
||||
}); err != nil {
|
||||
ctx.Errorf("unexpected memcache.Set error: %v", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
// aeMemcache wraps the needed Memcache functionality to make it easy to mock
|
||||
type aeMemcache struct{}
|
||||
|
||||
@@ -32,99 +106,3 @@ type memcacher interface {
|
||||
Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error)
|
||||
Set(c appengine.Context, item *memcache.Item) error
|
||||
}
|
||||
|
||||
// memcacheGob enables mocking of the memcache.Gob calls for unit testing.
|
||||
var memcacheGob memcacher = &aeMemcache{}
|
||||
|
||||
// accessTokenFunc enables mocking of the appengine.AccessToken call for unit testing.
|
||||
var accessTokenFunc = appengine.AccessToken
|
||||
|
||||
// safetyMargin is used to avoid clock-skew problems.
|
||||
// 5 minutes is conservative because tokens are valid for 60 minutes.
|
||||
const safetyMargin = 5 * time.Minute
|
||||
|
||||
// mu protects multiple threads from attempting to fetch a token at the same time.
|
||||
var mu sync.Mutex
|
||||
|
||||
// tokens implements a local cache of tokens to prevent hitting quota limits for appengine.AccessToken calls.
|
||||
var tokens map[string]*oauth2.Token
|
||||
|
||||
func init() {
|
||||
tokens = make(map[string]*oauth2.Token)
|
||||
}
|
||||
|
||||
// AppEngineConfig represents a configuration for an
|
||||
// App Engine application's Google service account.
|
||||
type AppEngineConfig struct {
|
||||
// Transport is the http.RoundTripper to be used
|
||||
// to construct new oauth2.Transport instances from
|
||||
// this configuration.
|
||||
Transport http.RoundTripper
|
||||
|
||||
context appengine.Context
|
||||
scopes []string
|
||||
}
|
||||
|
||||
// NewAppEngineConfig creates a new AppEngineConfig for the
|
||||
// provided auth scopes.
|
||||
func NewAppEngineConfig(context appengine.Context, scopes ...string) *AppEngineConfig {
|
||||
return &AppEngineConfig{
|
||||
context: context,
|
||||
scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTransport returns a transport that authorizes
|
||||
// the requests with the application's service account.
|
||||
func (c *AppEngineConfig) NewTransport() *oauth2.Transport {
|
||||
return oauth2.NewTransport(c.transport(), c, nil)
|
||||
}
|
||||
|
||||
// FetchToken fetches a new access token for the provided scopes.
|
||||
// Tokens are cached locally and also with Memcache so that the app can scale
|
||||
// without hitting quota limits by calling appengine.AccessToken too frequently.
|
||||
func (c *AppEngineConfig) FetchToken(existing *oauth2.Token) (*oauth2.Token, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
key := ":" + strings.Join(c.scopes, "_")
|
||||
now := time.Now().Add(safetyMargin)
|
||||
if t, ok := tokens[key]; ok && !t.Expiry.Before(now) {
|
||||
return t, nil
|
||||
}
|
||||
delete(tokens, key)
|
||||
|
||||
// Attempt to get token from Memcache
|
||||
tok := new(oauth2.Token)
|
||||
_, err := memcacheGob.Get(c.context, key, tok)
|
||||
if err == nil && !tok.Expiry.Before(now) {
|
||||
tokens[key] = tok // Save token locally
|
||||
return tok, nil
|
||||
}
|
||||
|
||||
token, expiry, err := accessTokenFunc(c.context, c.scopes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
AccessToken: token,
|
||||
Expiry: expiry,
|
||||
}
|
||||
tokens[key] = t
|
||||
// Also back up token in Memcache
|
||||
if err = memcacheGob.Set(c.context, &memcache.Item{
|
||||
Key: key,
|
||||
Value: []byte{},
|
||||
Object: *t,
|
||||
Expiration: expiry.Sub(now),
|
||||
}); err != nil {
|
||||
c.context.Errorf("unexpected memcache.Set error: %v", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *AppEngineConfig) transport() http.RoundTripper {
|
||||
if c.Transport != nil {
|
||||
return c.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package google
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -71,11 +72,16 @@ func TestFetchTokenLocalCacheMiss(t *testing.T) {
|
||||
memcacheGob = m
|
||||
accessTokenCount = 0
|
||||
delete(tokens, testScopeKey) // clear local cache
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
tr := f.NewTransport()
|
||||
c := http.Client{Transport: tr}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -101,8 +107,16 @@ func TestFetchTokenLocalCacheHit(t *testing.T) {
|
||||
AccessToken: "mytoken",
|
||||
Expiry: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
tr := f.NewTransport()
|
||||
c := http.Client{Transport: tr}
|
||||
c.Get("server")
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
}
|
||||
@@ -138,11 +152,16 @@ func TestFetchTokenMemcacheHit(t *testing.T) {
|
||||
Expiration: 1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -179,11 +198,15 @@ func TestFetchTokenLocalCacheExpired(t *testing.T) {
|
||||
Expiration: 1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
@@ -216,11 +239,15 @@ func TestFetchTokenMemcacheExpired(t *testing.T) {
|
||||
Expiration: -1 * time.Hour,
|
||||
})
|
||||
m.setCount = 0
|
||||
config := NewAppEngineConfig(nil, testScope)
|
||||
_, err := config.FetchToken(nil)
|
||||
f, err := oauth2.New(
|
||||
AppEngineContext(nil),
|
||||
oauth2.Scope(testScope),
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unable to FetchToken: %v", err)
|
||||
t.Error(err)
|
||||
}
|
||||
c := http.Client{Transport: f.NewTransport()}
|
||||
c.Get("server")
|
||||
if w := 1; m.getCount != w {
|
||||
t.Errorf("bad memcache.Get count: got %v, want %v", m.getCount, w)
|
||||
}
|
||||
|
||||
@@ -24,25 +24,25 @@ func TestA(t *testing.T) {}
|
||||
func Example_webServer() {
|
||||
// Your credentials should be obtained from the Google
|
||||
// Developer Console (https://console.developers.google.com).
|
||||
config, err := google.NewConfig(&oauth2.Options{
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
RedirectURL: "YOUR_REDIRECT_URL",
|
||||
Scopes: []string{
|
||||
f, err := oauth2.New(
|
||||
oauth2.Client("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"),
|
||||
oauth2.RedirectURL("YOUR_REDIRECT_URL"),
|
||||
oauth2.Scope(
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/blogger"},
|
||||
})
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
),
|
||||
google.Endpoint(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Redirect user to Google's consent page to ask for permission
|
||||
// for the scopes specified above.
|
||||
url := config.AuthCodeURL("state", "online", "auto")
|
||||
url := f.AuthCodeURL("state", "online", "auto")
|
||||
fmt.Printf("Visit the URL for the auth dialog: %v", url)
|
||||
|
||||
// Handle the exchange code to initiate a transport
|
||||
t, err := config.NewTransportWithCode("exchange-code")
|
||||
t, err := f.NewTransportFromCode("exchange-code")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -58,9 +58,12 @@ func Example_serviceAccountsJSON() {
|
||||
// To create a service account client, click "Create new Client ID",
|
||||
// select "Service Account", and click "Create Client ID". A JSON
|
||||
// key file will then be downloaded to your computer.
|
||||
config, err := google.NewServiceAccountJSONConfig(
|
||||
"/path/to/your-project-key.json",
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
f, err := oauth2.New(
|
||||
google.ServiceAccountJSONKey("/path/to/your-project-key.json"),
|
||||
oauth2.Scope(
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -68,63 +71,74 @@ func Example_serviceAccountsJSON() {
|
||||
// Initiate an http.Client. The following GET request will be
|
||||
// authorized and authenticated on the behalf of
|
||||
// your service account.
|
||||
client := http.Client{Transport: config.NewTransport()}
|
||||
client.Get("...")
|
||||
|
||||
// If you would like to impersonate a user, you can
|
||||
// create a transport with a subject. The following GET
|
||||
// request will be made on the behalf of user@example.com.
|
||||
client = http.Client{Transport: config.NewTransportWithUser("user@example.com")}
|
||||
client := http.Client{Transport: f.NewTransport()}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func Example_serviceAccounts() {
|
||||
// Your credentials should be obtained from the Google
|
||||
// Developer Console (https://console.developers.google.com).
|
||||
config, err := google.NewServiceAccountConfig(&oauth2.JWTOptions{
|
||||
Email: "xxx@developer.gserviceaccount.com",
|
||||
f, err := oauth2.New(
|
||||
// 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.
|
||||
// can use `openssl` to export the private key into a pem file.
|
||||
//
|
||||
// $ openssl pkcs12 -in key.p12 -out key.pem -nodes
|
||||
//
|
||||
// Supports only PEM containers without a passphrase.
|
||||
PrivateKey: []byte("PRIVATE KEY CONTENTS"),
|
||||
Scopes: []string{
|
||||
// It only supports PEM containers with no passphrase.
|
||||
oauth2.JWTClient(
|
||||
"xxx@developer.gserviceaccount.com",
|
||||
[]byte("-----BEGIN RSA PRIVATE KEY-----...")),
|
||||
oauth2.Scope(
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
},
|
||||
})
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
),
|
||||
google.JWTEndpoint(),
|
||||
// If you would like to impersonate a user, you can
|
||||
// create a transport with a subject. The following GET
|
||||
// request will be made on the behalf of user@example.com.
|
||||
// Subject is optional.
|
||||
oauth2.Subject("user@example.com"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Initiate an http.Client, the following GET request will be
|
||||
// authorized and authenticated on the behalf of
|
||||
// xxx@developer.gserviceaccount.com.
|
||||
client := http.Client{Transport: config.NewTransport()}
|
||||
client.Get("...")
|
||||
|
||||
// If you would like to impersonate a user, you can
|
||||
// create a transport with a subject. The following GET
|
||||
// request will be made on the behalf of user@example.com.
|
||||
client = http.Client{Transport: config.NewTransportWithUser("user@example.com")}
|
||||
// authorized and authenticated on the behalf of user@example.com.
|
||||
client := http.Client{Transport: f.NewTransport()}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func Example_appEngine() {
|
||||
c := appengine.NewContext(nil)
|
||||
config := google.NewAppEngineConfig(c, "https://www.googleapis.com/auth/bigquery")
|
||||
func Example_appEngineVMs() {
|
||||
ctx := appengine.NewContext(nil)
|
||||
f, err := oauth2.New(
|
||||
google.AppEngineContext(ctx),
|
||||
oauth2.Scope(
|
||||
"https://www.googleapis.com/auth/bigquery",
|
||||
"https://www.googleapis.com/auth/blogger",
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// The following client will be authorized by the App Engine
|
||||
// app's service account for the provided scopes.
|
||||
client := http.Client{Transport: config.NewTransport()}
|
||||
client := http.Client{Transport: f.NewTransport()}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
func Example_computeEngine() {
|
||||
// If no other account is specified, "default" is used.
|
||||
config := google.NewComputeEngineConfig("")
|
||||
client := http.Client{Transport: config.NewTransport()}
|
||||
f, err := oauth2.New(
|
||||
// Query Google Compute Engine's metadata server to retrieve
|
||||
// an access token for the provided account.
|
||||
// If no account is specified, "default" is used.
|
||||
google.ComputeEngineAccount(""),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
client := http.Client{Transport: f.NewTransport()}
|
||||
client.Get("...")
|
||||
}
|
||||
|
||||
189
google/google.go
189
google/google.go
@@ -15,18 +15,20 @@ package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/golang/oauth2"
|
||||
"github.com/golang/oauth2/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// Google endpoints.
|
||||
uriGoogleAuth = "https://accounts.google.com/o/oauth2/auth"
|
||||
uriGoogleToken = "https://accounts.google.com/o/oauth2/token"
|
||||
var (
|
||||
uriGoogleAuth, _ = url.Parse("https://accounts.google.com/o/oauth2/auth")
|
||||
uriGoogleToken, _ = url.Parse("https://accounts.google.com/o/oauth2/token")
|
||||
)
|
||||
|
||||
type metaTokenRespBody struct {
|
||||
@@ -35,112 +37,93 @@ type metaTokenRespBody struct {
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// ComputeEngineConfig represents a OAuth 2.0 consumer client
|
||||
// running on Google Compute Engine.
|
||||
type ComputeEngineConfig struct {
|
||||
// Client is the HTTP client to be used to retrieve
|
||||
// tokens from the OAuth 2.0 provider.
|
||||
Client *http.Client
|
||||
|
||||
// Transport is the round tripper to be used
|
||||
// to construct new oauth2.Transport instances from
|
||||
// this configuration.
|
||||
Transport http.RoundTripper
|
||||
|
||||
account string
|
||||
// JWTEndpoint adds the endpoints required to complete the 2-legged service account flow.
|
||||
func JWTEndpoint() oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
opts.AUD = uriGoogleToken
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfig creates a new OAuth2 config that uses Google
|
||||
// endpoints.
|
||||
func NewConfig(opts *oauth2.Options) (*oauth2.Config, error) {
|
||||
return oauth2.NewConfig(opts, uriGoogleAuth, uriGoogleToken)
|
||||
// Endpoint adds the endpoints required to do the 3-legged Web server flow.
|
||||
func Endpoint() oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
opts.AuthURL = uriGoogleAuth
|
||||
opts.TokenURL = uriGoogleToken
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewServiceAccountConfig creates a new JWT config that can
|
||||
// fetch Bearer JWT tokens from Google endpoints.
|
||||
func NewServiceAccountConfig(opts *oauth2.JWTOptions) (*oauth2.JWTConfig, error) {
|
||||
return oauth2.NewJWTConfig(opts, uriGoogleToken)
|
||||
// ComputeEngineAccount uses the specified account to retrieve an access
|
||||
// token from the Google Compute Engine's metadata server. If no user is
|
||||
// provided, "default" is being used.
|
||||
func ComputeEngineAccount(account string) oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
if account == "" {
|
||||
account = "default"
|
||||
}
|
||||
opts.TokenFetcherFunc = makeComputeFetcher(opts, account)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewServiceAccountJSONConfig creates a new JWT config from a
|
||||
// JSON key file downloaded from the Google Developers Console.
|
||||
// See the "Credentials" page under "APIs & Auth" for your project
|
||||
// at https://console.developers.google.com.
|
||||
func NewServiceAccountJSONConfig(filename string, scopes ...string) (*oauth2.JWTConfig, error) {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// ServiceAccountJSONKey uses the provided Google Developers
|
||||
// JSON key file to authorize the user. See the "Credentials" page under
|
||||
// "APIs & Auth" for your project at https://console.developers.google.com
|
||||
// to download a JSON key file.
|
||||
func ServiceAccountJSONKey(filename string) oauth2.Option {
|
||||
return func(opts *oauth2.Options) error {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var key struct {
|
||||
Email string `json:"client_email"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &key); err != nil {
|
||||
return err
|
||||
}
|
||||
pk, err := internal.ParseKey([]byte(key.PrivateKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Email = key.Email
|
||||
opts.PrivateKey = pk
|
||||
opts.AUD = uriGoogleToken
|
||||
return nil
|
||||
}
|
||||
var key struct {
|
||||
Email string `json:"client_email"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := &oauth2.JWTOptions{
|
||||
Email: key.Email,
|
||||
PrivateKey: []byte(key.PrivateKey),
|
||||
Scopes: scopes,
|
||||
}
|
||||
return NewServiceAccountConfig(opts)
|
||||
}
|
||||
|
||||
// NewComputeEngineConfig creates a new config that can fetch tokens
|
||||
// from Google Compute Engine instance's metaserver. If no account is
|
||||
// provided, default is used.
|
||||
func NewComputeEngineConfig(account string) *ComputeEngineConfig {
|
||||
return &ComputeEngineConfig{account: account}
|
||||
}
|
||||
|
||||
// NewTransport creates an authorized transport.
|
||||
func (c *ComputeEngineConfig) NewTransport() *oauth2.Transport {
|
||||
return oauth2.NewTransport(c.transport(), c, nil)
|
||||
}
|
||||
|
||||
// FetchToken retrieves a new access token via metadata server.
|
||||
func (c *ComputeEngineConfig) FetchToken(existing *oauth2.Token) (token *oauth2.Token, err error) {
|
||||
account := "default"
|
||||
if c.account != "" {
|
||||
account = c.account
|
||||
}
|
||||
u := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/" + account + "/token"
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Add("X-Google-Metadata-Request", "True")
|
||||
resp, err := c.client().Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf("oauth2: can't retrieve a token from metadata server, status code: %d", resp.StatusCode)
|
||||
}
|
||||
var tokenResp metaTokenRespBody
|
||||
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
token = &oauth2.Token{
|
||||
AccessToken: tokenResp.AccessToken,
|
||||
TokenType: tokenResp.TokenType,
|
||||
Expiry: time.Now().Add(tokenResp.ExpiresIn * time.Second),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ComputeEngineConfig) transport() http.RoundTripper {
|
||||
if c.Transport != nil {
|
||||
return c.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
func (c *ComputeEngineConfig) client() *http.Client {
|
||||
if c.Client != nil {
|
||||
return c.Client
|
||||
}
|
||||
return http.DefaultClient
|
||||
func makeComputeFetcher(opts *oauth2.Options, account string) func(*oauth2.Token) (*oauth2.Token, error) {
|
||||
return func(t *oauth2.Token) (*oauth2.Token, error) {
|
||||
u := "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/" + account + "/token"
|
||||
req, err := http.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("X-Google-Metadata-Request", "True")
|
||||
c := &http.Client{}
|
||||
if opts.Client != nil {
|
||||
c = opts.Client
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf("oauth2: can't retrieve a token from metadata server, status code: %d", resp.StatusCode)
|
||||
}
|
||||
var tokenResp metaTokenRespBody
|
||||
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &oauth2.Token{
|
||||
AccessToken: tokenResp.AccessToken,
|
||||
TokenType: tokenResp.TokenType,
|
||||
Expiry: time.Now().Add(tokenResp.ExpiresIn * time.Second),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user