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