| // 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. |
| |
| // +build appengine,!appenginevm |
| |
| package google |
| |
| import ( |
| "net/http" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/oauth2" |
| |
| "appengine" |
| "appengine/memcache" |
| "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.Client = &http.Client{ |
| 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{} |
| |
| func (m *aeMemcache) Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error) { |
| return memcache.Gob.Get(c, key, tok) |
| } |
| |
| func (m *aeMemcache) Set(c appengine.Context, item *memcache.Item) error { |
| return memcache.Gob.Set(c, item) |
| } |
| |
| type memcacher interface { |
| Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error) |
| Set(c appengine.Context, item *memcache.Item) error |
| } |