| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114 |
- // 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"
- "github.com/golang/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
- }
|