You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

194 lines
5.9 KiB

  1. // Copyright 2014 The Go 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. package google
  5. import (
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "strings"
  11. "time"
  12. "cloud.google.com/go/compute/metadata"
  13. "golang.org/x/oauth2"
  14. "golang.org/x/oauth2/jwt"
  15. )
  16. // Endpoint is Google's OAuth 2.0 endpoint.
  17. var Endpoint = oauth2.Endpoint{
  18. AuthURL: "https://accounts.google.com/o/oauth2/auth",
  19. TokenURL: "https://oauth2.googleapis.com/token",
  20. AuthStyle: oauth2.AuthStyleInParams,
  21. }
  22. // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
  23. const JWTTokenURL = "https://oauth2.googleapis.com/token"
  24. // ConfigFromJSON uses a Google Developers Console client_credentials.json
  25. // file to construct a config.
  26. // client_credentials.json can be downloaded from
  27. // https://console.developers.google.com, under "Credentials". Download the Web
  28. // application credentials in the JSON format and provide the contents of the
  29. // file as jsonKey.
  30. func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
  31. type cred struct {
  32. ClientID string `json:"client_id"`
  33. ClientSecret string `json:"client_secret"`
  34. RedirectURIs []string `json:"redirect_uris"`
  35. AuthURI string `json:"auth_uri"`
  36. TokenURI string `json:"token_uri"`
  37. }
  38. var j struct {
  39. Web *cred `json:"web"`
  40. Installed *cred `json:"installed"`
  41. }
  42. if err := json.Unmarshal(jsonKey, &j); err != nil {
  43. return nil, err
  44. }
  45. var c *cred
  46. switch {
  47. case j.Web != nil:
  48. c = j.Web
  49. case j.Installed != nil:
  50. c = j.Installed
  51. default:
  52. return nil, fmt.Errorf("oauth2/google: no credentials found")
  53. }
  54. if len(c.RedirectURIs) < 1 {
  55. return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
  56. }
  57. return &oauth2.Config{
  58. ClientID: c.ClientID,
  59. ClientSecret: c.ClientSecret,
  60. RedirectURL: c.RedirectURIs[0],
  61. Scopes: scope,
  62. Endpoint: oauth2.Endpoint{
  63. AuthURL: c.AuthURI,
  64. TokenURL: c.TokenURI,
  65. },
  66. }, nil
  67. }
  68. // JWTConfigFromJSON uses a Google Developers service account JSON key file to read
  69. // the credentials that authorize and authenticate the requests.
  70. // Create a service account on "Credentials" for your project at
  71. // https://console.developers.google.com to download a JSON key file.
  72. func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
  73. var f credentialsFile
  74. if err := json.Unmarshal(jsonKey, &f); err != nil {
  75. return nil, err
  76. }
  77. if f.Type != serviceAccountKey {
  78. return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
  79. }
  80. scope = append([]string(nil), scope...) // copy
  81. return f.jwtConfig(scope), nil
  82. }
  83. // JSON key file types.
  84. const (
  85. serviceAccountKey = "service_account"
  86. userCredentialsKey = "authorized_user"
  87. )
  88. // credentialsFile is the unmarshalled representation of a credentials file.
  89. type credentialsFile struct {
  90. Type string `json:"type"` // serviceAccountKey or userCredentialsKey
  91. // Service Account fields
  92. ClientEmail string `json:"client_email"`
  93. PrivateKeyID string `json:"private_key_id"`
  94. PrivateKey string `json:"private_key"`
  95. TokenURL string `json:"token_uri"`
  96. ProjectID string `json:"project_id"`
  97. // User Credential fields
  98. // (These typically come from gcloud auth.)
  99. ClientSecret string `json:"client_secret"`
  100. ClientID string `json:"client_id"`
  101. RefreshToken string `json:"refresh_token"`
  102. }
  103. func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config {
  104. cfg := &jwt.Config{
  105. Email: f.ClientEmail,
  106. PrivateKey: []byte(f.PrivateKey),
  107. PrivateKeyID: f.PrivateKeyID,
  108. Scopes: scopes,
  109. TokenURL: f.TokenURL,
  110. }
  111. if cfg.TokenURL == "" {
  112. cfg.TokenURL = JWTTokenURL
  113. }
  114. return cfg
  115. }
  116. func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) {
  117. switch f.Type {
  118. case serviceAccountKey:
  119. cfg := f.jwtConfig(scopes)
  120. return cfg.TokenSource(ctx), nil
  121. case userCredentialsKey:
  122. cfg := &oauth2.Config{
  123. ClientID: f.ClientID,
  124. ClientSecret: f.ClientSecret,
  125. Scopes: scopes,
  126. Endpoint: Endpoint,
  127. }
  128. tok := &oauth2.Token{RefreshToken: f.RefreshToken}
  129. return cfg.TokenSource(ctx, tok), nil
  130. case "":
  131. return nil, errors.New("missing 'type' field in credentials")
  132. default:
  133. return nil, fmt.Errorf("unknown credential type: %q", f.Type)
  134. }
  135. }
  136. // ComputeTokenSource returns a token source that fetches access tokens
  137. // from Google Compute Engine (GCE)'s metadata server. It's only valid to use
  138. // this token source if your program is running on a GCE instance.
  139. // If no account is specified, "default" is used.
  140. // Further information about retrieving access tokens from the GCE metadata
  141. // server can be found at https://cloud.google.com/compute/docs/authentication.
  142. func ComputeTokenSource(account string) oauth2.TokenSource {
  143. return oauth2.ReuseTokenSource(nil, computeSource{account: account})
  144. }
  145. type computeSource struct {
  146. account string
  147. }
  148. func (cs computeSource) Token() (*oauth2.Token, error) {
  149. if !metadata.OnGCE() {
  150. return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
  151. }
  152. acct := cs.account
  153. if acct == "" {
  154. acct = "default"
  155. }
  156. tokenJSON, err := metadata.Get("instance/service-accounts/" + acct + "/token")
  157. if err != nil {
  158. return nil, err
  159. }
  160. var res struct {
  161. AccessToken string `json:"access_token"`
  162. ExpiresInSec int `json:"expires_in"`
  163. TokenType string `json:"token_type"`
  164. }
  165. err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
  166. if err != nil {
  167. return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
  168. }
  169. if res.ExpiresInSec == 0 || res.AccessToken == "" {
  170. return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
  171. }
  172. return &oauth2.Token{
  173. AccessToken: res.AccessToken,
  174. TokenType: res.TokenType,
  175. Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
  176. }, nil
  177. }