appengine.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. // Copyright 2014 The oauth2 Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // +build appengine,!appenginevm
  5. package google
  6. import (
  7. "net/http"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/golang/oauth2"
  12. "appengine"
  13. "appengine/memcache"
  14. "appengine/urlfetch"
  15. )
  16. var (
  17. // memcacheGob enables mocking of the memcache.Gob calls for unit testing.
  18. memcacheGob memcacher = &aeMemcache{}
  19. // accessTokenFunc enables mocking of the appengine.AccessToken call for unit testing.
  20. accessTokenFunc = appengine.AccessToken
  21. // mu protects multiple threads from attempting to fetch a token at the same time.
  22. mu sync.Mutex
  23. // tokens implements a local cache of tokens to prevent hitting quota limits for appengine.AccessToken calls.
  24. tokens map[string]*oauth2.Token
  25. )
  26. // safetyMargin is used to avoid clock-skew problems.
  27. // 5 minutes is conservative because tokens are valid for 60 minutes.
  28. const safetyMargin = 5 * time.Minute
  29. func init() {
  30. tokens = make(map[string]*oauth2.Token)
  31. }
  32. // AppEngineContext requires an App Engine request context.
  33. func AppEngineContext(ctx appengine.Context) oauth2.Option {
  34. return func(opts *oauth2.Options) error {
  35. opts.TokenFetcherFunc = makeAppEngineTokenFetcher(ctx, opts)
  36. opts.Client = &http.Client{
  37. Transport: &urlfetch.Transport{Context: ctx},
  38. }
  39. return nil
  40. }
  41. }
  42. // FetchToken fetches a new access token for the provided scopes.
  43. // Tokens are cached locally and also with Memcache so that the app can scale
  44. // without hitting quota limits by calling appengine.AccessToken too frequently.
  45. func makeAppEngineTokenFetcher(ctx appengine.Context, opts *oauth2.Options) func(*oauth2.Token) (*oauth2.Token, error) {
  46. return func(existing *oauth2.Token) (*oauth2.Token, error) {
  47. mu.Lock()
  48. defer mu.Unlock()
  49. key := ":" + strings.Join(opts.Scopes, "_")
  50. now := time.Now().Add(safetyMargin)
  51. if t, ok := tokens[key]; ok && !t.Expiry.Before(now) {
  52. return t, nil
  53. }
  54. delete(tokens, key)
  55. // Attempt to get token from Memcache
  56. tok := new(oauth2.Token)
  57. _, err := memcacheGob.Get(ctx, key, tok)
  58. if err == nil && !tok.Expiry.Before(now) {
  59. tokens[key] = tok // Save token locally
  60. return tok, nil
  61. }
  62. token, expiry, err := accessTokenFunc(ctx, opts.Scopes...)
  63. if err != nil {
  64. return nil, err
  65. }
  66. t := &oauth2.Token{
  67. AccessToken: token,
  68. Expiry: expiry,
  69. }
  70. tokens[key] = t
  71. // Also back up token in Memcache
  72. if err = memcacheGob.Set(ctx, &memcache.Item{
  73. Key: key,
  74. Value: []byte{},
  75. Object: *t,
  76. Expiration: expiry.Sub(now),
  77. }); err != nil {
  78. ctx.Errorf("unexpected memcache.Set error: %v", err)
  79. }
  80. return t, nil
  81. }
  82. }
  83. // aeMemcache wraps the needed Memcache functionality to make it easy to mock
  84. type aeMemcache struct{}
  85. func (m *aeMemcache) Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error) {
  86. return memcache.Gob.Get(c, key, tok)
  87. }
  88. func (m *aeMemcache) Set(c appengine.Context, item *memcache.Item) error {
  89. return memcache.Gob.Set(c, item)
  90. }
  91. type memcacher interface {
  92. Get(c appengine.Context, key string, tok *oauth2.Token) (*memcache.Item, error)
  93. Set(c appengine.Context, item *memcache.Item) error
  94. }