| // Copyright 2023 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package credentials | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "net/url" | |
| "strings" | |
| "time" | |
| "cloud.google.com/go/auth" | |
| "cloud.google.com/go/compute/metadata" | |
| ) | |
| var ( | |
| computeTokenMetadata = map[string]interface{}{ | |
| "auth.google.tokenSource": "compute-metadata", | |
| "auth.google.serviceAccount": "default", | |
| } | |
| computeTokenURI = "instance/service-accounts/default/token" | |
| ) | |
| // computeTokenProvider creates a [cloud.google.com/go/auth.TokenProvider] that | |
| // uses the metadata service to retrieve tokens. | |
| func computeTokenProvider(opts *DetectOptions, client *metadata.Client) auth.TokenProvider { | |
| return auth.NewCachedTokenProvider(&computeProvider{ | |
| scopes: opts.Scopes, | |
| client: client, | |
| tokenBindingType: opts.TokenBindingType, | |
| }, &auth.CachedTokenProviderOptions{ | |
| ExpireEarly: opts.EarlyTokenRefresh, | |
| DisableAsyncRefresh: opts.DisableAsyncRefresh, | |
| }) | |
| } | |
| // computeProvider fetches tokens from the google cloud metadata service. | |
| type computeProvider struct { | |
| scopes []string | |
| client *metadata.Client | |
| tokenBindingType TokenBindingType | |
| } | |
| type metadataTokenResp struct { | |
| AccessToken string `json:"access_token"` | |
| ExpiresInSec int `json:"expires_in"` | |
| TokenType string `json:"token_type"` | |
| } | |
| func (cs *computeProvider) Token(ctx context.Context) (*auth.Token, error) { | |
| tokenURI, err := url.Parse(computeTokenURI) | |
| if err != nil { | |
| return nil, err | |
| } | |
| hasScopes := len(cs.scopes) > 0 | |
| if hasScopes || cs.tokenBindingType != NoBinding { | |
| v := url.Values{} | |
| if hasScopes { | |
| v.Set("scopes", strings.Join(cs.scopes, ",")) | |
| } | |
| switch cs.tokenBindingType { | |
| case MTLSHardBinding: | |
| v.Set("transport", "mtls") | |
| v.Set("binding-enforcement", "on") | |
| case ALTSHardBinding: | |
| v.Set("transport", "alts") | |
| } | |
| tokenURI.RawQuery = v.Encode() | |
| } | |
| tokenJSON, err := cs.client.GetWithContext(ctx, tokenURI.String()) | |
| if err != nil { | |
| return nil, fmt.Errorf("credentials: cannot fetch token: %w", err) | |
| } | |
| var res metadataTokenResp | |
| if err := json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res); err != nil { | |
| return nil, fmt.Errorf("credentials: invalid token JSON from metadata: %w", err) | |
| } | |
| if res.ExpiresInSec == 0 || res.AccessToken == "" { | |
| return nil, errors.New("credentials: incomplete token received from metadata") | |
| } | |
| token := &auth.Token{ | |
| Value: res.AccessToken, | |
| Type: res.TokenType, | |
| Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second), | |
| Metadata: computeTokenMetadata, | |
| } | |
| return token, nil | |
| } |