google.go raw
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
5 package google
6
7 import (
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/google/externalaccount"
19 "golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
20 "golang.org/x/oauth2/google/internal/impersonate"
21 "golang.org/x/oauth2/jwt"
22 )
23
24 // Endpoint is Google's OAuth 2.0 default endpoint.
25 var Endpoint = oauth2.Endpoint{
26 AuthURL: "https://accounts.google.com/o/oauth2/auth",
27 TokenURL: "https://oauth2.googleapis.com/token",
28 DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
29 AuthStyle: oauth2.AuthStyleInParams,
30 }
31
32 // MTLSTokenURL is Google's OAuth 2.0 default mTLS endpoint.
33 const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
34
35 // JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
36 const JWTTokenURL = "https://oauth2.googleapis.com/token"
37
38 // ConfigFromJSON uses a Google Developers Console client_credentials.json
39 // file to construct a config.
40 // client_credentials.json can be downloaded from
41 // https://console.developers.google.com, under "Credentials". Download the Web
42 // application credentials in the JSON format and provide the contents of the
43 // file as jsonKey.
44 func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
45 type cred struct {
46 ClientID string `json:"client_id"`
47 ClientSecret string `json:"client_secret"`
48 RedirectURIs []string `json:"redirect_uris"`
49 AuthURI string `json:"auth_uri"`
50 TokenURI string `json:"token_uri"`
51 }
52 var j struct {
53 Web *cred `json:"web"`
54 Installed *cred `json:"installed"`
55 }
56 if err := json.Unmarshal(jsonKey, &j); err != nil {
57 return nil, err
58 }
59 var c *cred
60 switch {
61 case j.Web != nil:
62 c = j.Web
63 case j.Installed != nil:
64 c = j.Installed
65 default:
66 return nil, fmt.Errorf("oauth2/google: no credentials found")
67 }
68 if len(c.RedirectURIs) < 1 {
69 return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
70 }
71 return &oauth2.Config{
72 ClientID: c.ClientID,
73 ClientSecret: c.ClientSecret,
74 RedirectURL: c.RedirectURIs[0],
75 Scopes: scope,
76 Endpoint: oauth2.Endpoint{
77 AuthURL: c.AuthURI,
78 TokenURL: c.TokenURI,
79 },
80 }, nil
81 }
82
83 // JWTConfigFromJSON uses a Google Developers service account JSON key file to read
84 // the credentials that authorize and authenticate the requests.
85 // Create a service account on "Credentials" for your project at
86 // https://console.developers.google.com to download a JSON key file.
87 func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
88 var f credentialsFile
89 if err := json.Unmarshal(jsonKey, &f); err != nil {
90 return nil, err
91 }
92 if f.Type != serviceAccountKey {
93 return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
94 }
95 scope = append([]string(nil), scope...) // copy
96 return f.jwtConfig(scope, ""), nil
97 }
98
99 // JSON key file types.
100 const (
101 serviceAccountKey = "service_account"
102 userCredentialsKey = "authorized_user"
103 externalAccountKey = "external_account"
104 externalAccountAuthorizedUserKey = "external_account_authorized_user"
105 impersonatedServiceAccount = "impersonated_service_account"
106 )
107
108 // credentialsFile is the unmarshalled representation of a credentials file.
109 type credentialsFile struct {
110 Type string `json:"type"`
111
112 // Service Account fields
113 ClientEmail string `json:"client_email"`
114 PrivateKeyID string `json:"private_key_id"`
115 PrivateKey string `json:"private_key"`
116 AuthURL string `json:"auth_uri"`
117 TokenURL string `json:"token_uri"`
118 ProjectID string `json:"project_id"`
119 UniverseDomain string `json:"universe_domain"`
120
121 // User Credential fields
122 // (These typically come from gcloud auth.)
123 ClientSecret string `json:"client_secret"`
124 ClientID string `json:"client_id"`
125 RefreshToken string `json:"refresh_token"`
126
127 // External Account fields
128 Audience string `json:"audience"`
129 SubjectTokenType string `json:"subject_token_type"`
130 TokenURLExternal string `json:"token_url"`
131 TokenInfoURL string `json:"token_info_url"`
132 ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
133 ServiceAccountImpersonation serviceAccountImpersonationInfo `json:"service_account_impersonation"`
134 Delegates []string `json:"delegates"`
135 CredentialSource externalaccount.CredentialSource `json:"credential_source"`
136 QuotaProjectID string `json:"quota_project_id"`
137 WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
138
139 // External Account Authorized User fields
140 RevokeURL string `json:"revoke_url"`
141
142 // Service account impersonation
143 SourceCredentials *credentialsFile `json:"source_credentials"`
144 }
145
146 type serviceAccountImpersonationInfo struct {
147 TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
148 }
149
150 func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
151 cfg := &jwt.Config{
152 Email: f.ClientEmail,
153 PrivateKey: []byte(f.PrivateKey),
154 PrivateKeyID: f.PrivateKeyID,
155 Scopes: scopes,
156 TokenURL: f.TokenURL,
157 Subject: subject, // This is the user email to impersonate
158 Audience: f.Audience,
159 }
160 if cfg.TokenURL == "" {
161 cfg.TokenURL = JWTTokenURL
162 }
163 return cfg
164 }
165
166 func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
167 switch f.Type {
168 case serviceAccountKey:
169 cfg := f.jwtConfig(params.Scopes, params.Subject)
170 return cfg.TokenSource(ctx), nil
171 case userCredentialsKey:
172 cfg := &oauth2.Config{
173 ClientID: f.ClientID,
174 ClientSecret: f.ClientSecret,
175 Scopes: params.Scopes,
176 Endpoint: oauth2.Endpoint{
177 AuthURL: f.AuthURL,
178 TokenURL: f.TokenURL,
179 AuthStyle: oauth2.AuthStyleInParams,
180 },
181 }
182 if cfg.Endpoint.AuthURL == "" {
183 cfg.Endpoint.AuthURL = Endpoint.AuthURL
184 }
185 if cfg.Endpoint.TokenURL == "" {
186 if params.TokenURL != "" {
187 cfg.Endpoint.TokenURL = params.TokenURL
188 } else {
189 cfg.Endpoint.TokenURL = Endpoint.TokenURL
190 }
191 }
192 tok := &oauth2.Token{RefreshToken: f.RefreshToken}
193 return cfg.TokenSource(ctx, tok), nil
194 case externalAccountKey:
195 cfg := &externalaccount.Config{
196 Audience: f.Audience,
197 SubjectTokenType: f.SubjectTokenType,
198 TokenURL: f.TokenURLExternal,
199 TokenInfoURL: f.TokenInfoURL,
200 ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
201 ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
202 ClientSecret: f.ClientSecret,
203 ClientID: f.ClientID,
204 CredentialSource: &f.CredentialSource,
205 QuotaProjectID: f.QuotaProjectID,
206 Scopes: params.Scopes,
207 WorkforcePoolUserProject: f.WorkforcePoolUserProject,
208 }
209 return externalaccount.NewTokenSource(ctx, *cfg)
210 case externalAccountAuthorizedUserKey:
211 cfg := &externalaccountauthorizeduser.Config{
212 Audience: f.Audience,
213 RefreshToken: f.RefreshToken,
214 TokenURL: f.TokenURLExternal,
215 TokenInfoURL: f.TokenInfoURL,
216 ClientID: f.ClientID,
217 ClientSecret: f.ClientSecret,
218 RevokeURL: f.RevokeURL,
219 QuotaProjectID: f.QuotaProjectID,
220 Scopes: params.Scopes,
221 }
222 return cfg.TokenSource(ctx)
223 case impersonatedServiceAccount:
224 if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
225 return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
226 }
227
228 ts, err := f.SourceCredentials.tokenSource(ctx, params)
229 if err != nil {
230 return nil, err
231 }
232 imp := impersonate.ImpersonateTokenSource{
233 Ctx: ctx,
234 URL: f.ServiceAccountImpersonationURL,
235 Scopes: params.Scopes,
236 Ts: ts,
237 Delegates: f.Delegates,
238 }
239 return oauth2.ReuseTokenSource(nil, imp), nil
240 case "":
241 return nil, errors.New("missing 'type' field in credentials")
242 default:
243 return nil, fmt.Errorf("unknown credential type: %q", f.Type)
244 }
245 }
246
247 // ComputeTokenSource returns a token source that fetches access tokens
248 // from Google Compute Engine (GCE)'s metadata server. It's only valid to use
249 // this token source if your program is running on a GCE instance.
250 // If no account is specified, "default" is used.
251 // If no scopes are specified, a set of default scopes are automatically granted.
252 // Further information about retrieving access tokens from the GCE metadata
253 // server can be found at https://cloud.google.com/compute/docs/authentication.
254 func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
255 // Refresh 3 minutes and 45 seconds early. The shortest MDS cache is currently 4 minutes, so any
256 // refreshes earlier are a waste of compute.
257 earlyExpirySecs := 225 * time.Second
258 return computeTokenSource(account, earlyExpirySecs, scope...)
259 }
260
261 func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
262 return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
263 }
264
265 type computeSource struct {
266 account string
267 scopes []string
268 }
269
270 func (cs computeSource) Token() (*oauth2.Token, error) {
271 if !metadata.OnGCE() {
272 return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
273 }
274 acct := cs.account
275 if acct == "" {
276 acct = "default"
277 }
278 tokenURI := "instance/service-accounts/" + acct + "/token"
279 if len(cs.scopes) > 0 {
280 v := url.Values{}
281 v.Set("scopes", strings.Join(cs.scopes, ","))
282 tokenURI = tokenURI + "?" + v.Encode()
283 }
284 tokenJSON, err := metadata.Get(tokenURI)
285 if err != nil {
286 return nil, err
287 }
288 var res oauth2.Token
289 err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
290 if err != nil {
291 return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
292 }
293 if res.ExpiresIn == 0 || res.AccessToken == "" {
294 return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
295 }
296 tok := &oauth2.Token{
297 AccessToken: res.AccessToken,
298 TokenType: res.TokenType,
299 Expiry: time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
300 }
301 // NOTE(cbro): add hidden metadata about where the token is from.
302 // This is needed for detection by client libraries to know that credentials come from the metadata server.
303 // This may be removed in a future version of this library.
304 return tok.WithExtra(map[string]any{
305 "oauth2.google.tokenSource": "compute-metadata",
306 "oauth2.google.serviceAccount": acct,
307 }), nil
308 }
309