chore: migrate to gitea
This commit is contained in:
575
vendor/golang.org/x/oauth2/google/externalaccount/aws.go
generated
vendored
Normal file
575
vendor/golang.org/x/oauth2/google/externalaccount/aws.go
generated
vendored
Normal file
@@ -0,0 +1,575 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// AwsSecurityCredentials models AWS security credentials.
|
||||
type AwsSecurityCredentials struct {
|
||||
// AccessKeyID is the AWS Access Key ID - Required.
|
||||
AccessKeyID string `json:"AccessKeyID"`
|
||||
// SecretAccessKey is the AWS Secret Access Key - Required.
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
// SessionToken is the AWS Session token. This should be provided for temporary AWS security credentials - Optional.
|
||||
SessionToken string `json:"Token"`
|
||||
}
|
||||
|
||||
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
|
||||
type awsRequestSigner struct {
|
||||
RegionName string
|
||||
AwsSecurityCredentials *AwsSecurityCredentials
|
||||
}
|
||||
|
||||
// getenv aliases os.Getenv for testing
|
||||
var getenv = os.Getenv
|
||||
|
||||
const (
|
||||
defaultRegionalCredentialVerificationUrl = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
||||
|
||||
// AWS Signature Version 4 signing algorithm identifier.
|
||||
awsAlgorithm = "AWS4-HMAC-SHA256"
|
||||
|
||||
// The termination string for the AWS credential scope value as defined in
|
||||
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
awsRequestType = "aws4_request"
|
||||
|
||||
// The AWS authorization header name for the security session token if available.
|
||||
awsSecurityTokenHeader = "x-amz-security-token"
|
||||
|
||||
// The name of the header containing the session token for metadata endpoint calls
|
||||
awsIMDSv2SessionTokenHeader = "X-aws-ec2-metadata-token"
|
||||
|
||||
awsIMDSv2SessionTtlHeader = "X-aws-ec2-metadata-token-ttl-seconds"
|
||||
|
||||
awsIMDSv2SessionTtl = "300"
|
||||
|
||||
// The AWS authorization header name for the auto-generated date.
|
||||
awsDateHeader = "x-amz-date"
|
||||
|
||||
// Supported AWS configuration environment variables.
|
||||
awsAccessKeyId = "AWS_ACCESS_KEY_ID"
|
||||
awsDefaultRegion = "AWS_DEFAULT_REGION"
|
||||
awsRegion = "AWS_REGION"
|
||||
awsSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||
awsSessionToken = "AWS_SESSION_TOKEN"
|
||||
|
||||
awsTimeFormatLong = "20060102T150405Z"
|
||||
awsTimeFormatShort = "20060102"
|
||||
)
|
||||
|
||||
func getSha256(input []byte) (string, error) {
|
||||
hash := sha256.New()
|
||||
if _, err := hash.Write(input); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func getHmacSha256(key, input []byte) ([]byte, error) {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
if _, err := hash.Write(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
if r.Header != nil {
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
|
||||
// Find total number of values.
|
||||
headerCount := 0
|
||||
for _, headerValues := range r.Header {
|
||||
headerCount += len(headerValues)
|
||||
}
|
||||
copiedHeaders := make([]string, headerCount) // shared backing array for headers' values
|
||||
|
||||
for headerKey, headerValues := range r.Header {
|
||||
headerCount = copy(copiedHeaders, headerValues)
|
||||
r2.Header[headerKey] = copiedHeaders[:headerCount:headerCount]
|
||||
copiedHeaders = copiedHeaders[headerCount:]
|
||||
}
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
||||
func canonicalPath(req *http.Request) string {
|
||||
result := req.URL.EscapedPath()
|
||||
if result == "" {
|
||||
return "/"
|
||||
}
|
||||
return path.Clean(result)
|
||||
}
|
||||
|
||||
func canonicalQuery(req *http.Request) string {
|
||||
queryValues := req.URL.Query()
|
||||
for queryKey := range queryValues {
|
||||
sort.Strings(queryValues[queryKey])
|
||||
}
|
||||
return queryValues.Encode()
|
||||
}
|
||||
|
||||
func canonicalHeaders(req *http.Request) (string, string) {
|
||||
// Header keys need to be sorted alphabetically.
|
||||
var headers []string
|
||||
lowerCaseHeaders := make(http.Header)
|
||||
for k, v := range req.Header {
|
||||
k := strings.ToLower(k)
|
||||
if _, ok := lowerCaseHeaders[k]; ok {
|
||||
// include additional values
|
||||
lowerCaseHeaders[k] = append(lowerCaseHeaders[k], v...)
|
||||
} else {
|
||||
headers = append(headers, k)
|
||||
lowerCaseHeaders[k] = v
|
||||
}
|
||||
}
|
||||
sort.Strings(headers)
|
||||
|
||||
var fullHeaders strings.Builder
|
||||
for _, header := range headers {
|
||||
headerValue := strings.Join(lowerCaseHeaders[header], ",")
|
||||
fullHeaders.WriteString(header)
|
||||
fullHeaders.WriteByte(':')
|
||||
fullHeaders.WriteString(headerValue)
|
||||
fullHeaders.WriteByte('\n')
|
||||
}
|
||||
|
||||
return strings.Join(headers, ";"), fullHeaders.String()
|
||||
}
|
||||
|
||||
func requestDataHash(req *http.Request) (string, error) {
|
||||
var requestData []byte
|
||||
if req.Body != nil {
|
||||
requestBody, err := req.GetBody()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer requestBody.Close()
|
||||
|
||||
requestData, err = io.ReadAll(io.LimitReader(requestBody, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return getSha256(requestData)
|
||||
}
|
||||
|
||||
func requestHost(req *http.Request) string {
|
||||
if req.Host != "" {
|
||||
return req.Host
|
||||
}
|
||||
return req.URL.Host
|
||||
}
|
||||
|
||||
func canonicalRequest(req *http.Request, canonicalHeaderColumns, canonicalHeaderData string) (string, error) {
|
||||
dataHash, err := requestDataHash(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", req.Method, canonicalPath(req), canonicalQuery(req), canonicalHeaderData, canonicalHeaderColumns, dataHash), nil
|
||||
}
|
||||
|
||||
// SignRequest adds the appropriate headers to an http.Request
|
||||
// or returns an error if something prevented this.
|
||||
func (rs *awsRequestSigner) SignRequest(req *http.Request) error {
|
||||
signedRequest := cloneRequest(req)
|
||||
timestamp := now()
|
||||
|
||||
signedRequest.Header.Add("host", requestHost(req))
|
||||
|
||||
if rs.AwsSecurityCredentials.SessionToken != "" {
|
||||
signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
|
||||
}
|
||||
|
||||
if signedRequest.Header.Get("date") == "" {
|
||||
signedRequest.Header.Add(awsDateHeader, timestamp.Format(awsTimeFormatLong))
|
||||
}
|
||||
|
||||
authorizationCode, err := rs.generateAuthentication(signedRequest, timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedRequest.Header.Set("Authorization", authorizationCode)
|
||||
|
||||
req.Header = signedRequest.Header
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp time.Time) (string, error) {
|
||||
canonicalHeaderColumns, canonicalHeaderData := canonicalHeaders(req)
|
||||
|
||||
dateStamp := timestamp.Format(awsTimeFormatShort)
|
||||
serviceName := ""
|
||||
if splitHost := strings.Split(requestHost(req), "."); len(splitHost) > 0 {
|
||||
serviceName = splitHost[0]
|
||||
}
|
||||
|
||||
credentialScope := fmt.Sprintf("%s/%s/%s/%s", dateStamp, rs.RegionName, serviceName, awsRequestType)
|
||||
|
||||
requestString, err := canonicalRequest(req, canonicalHeaderColumns, canonicalHeaderData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
requestHash, err := getSha256([]byte(requestString))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", awsAlgorithm, timestamp.Format(awsTimeFormatLong), credentialScope, requestHash)
|
||||
|
||||
signingKey := []byte("AWS4" + rs.AwsSecurityCredentials.SecretAccessKey)
|
||||
for _, signingInput := range []string{
|
||||
dateStamp, rs.RegionName, serviceName, awsRequestType, stringToSign,
|
||||
} {
|
||||
signingKey, err = getHmacSha256(signingKey, []byte(signingInput))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", awsAlgorithm, rs.AwsSecurityCredentials.AccessKeyID, credentialScope, canonicalHeaderColumns, hex.EncodeToString(signingKey)), nil
|
||||
}
|
||||
|
||||
type awsCredentialSource struct {
|
||||
environmentID string
|
||||
regionURL string
|
||||
regionalCredVerificationURL string
|
||||
credVerificationURL string
|
||||
imdsv2SessionTokenURL string
|
||||
targetResource string
|
||||
requestSigner *awsRequestSigner
|
||||
region string
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
||||
supplierOptions SupplierOptions
|
||||
}
|
||||
|
||||
type awsRequestHeader struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type awsRequest struct {
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Headers []awsRequestHeader `json:"headers"`
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, error) {
|
||||
if cs.client == nil {
|
||||
cs.client = oauth2.NewClient(cs.ctx, nil)
|
||||
}
|
||||
return cs.client.Do(req.WithContext(cs.ctx))
|
||||
}
|
||||
|
||||
func canRetrieveRegionFromEnvironment() bool {
|
||||
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
|
||||
// required.
|
||||
return getenv(awsRegion) != "" || getenv(awsDefaultRegion) != ""
|
||||
}
|
||||
|
||||
func canRetrieveSecurityCredentialFromEnvironment() bool {
|
||||
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
|
||||
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) shouldUseMetadataServer() bool {
|
||||
return cs.awsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) credentialSourceType() string {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return "programmatic"
|
||||
}
|
||||
return "aws"
|
||||
}
|
||||
|
||||
func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||
// Set Defaults
|
||||
if cs.regionalCredVerificationURL == "" {
|
||||
cs.regionalCredVerificationURL = defaultRegionalCredentialVerificationUrl
|
||||
}
|
||||
if cs.requestSigner == nil {
|
||||
headers := make(map[string]string)
|
||||
if cs.shouldUseMetadataServer() {
|
||||
awsSessionToken, err := cs.getAWSSessionToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if awsSessionToken != "" {
|
||||
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
|
||||
}
|
||||
}
|
||||
|
||||
awsSecurityCredentials, err := cs.getSecurityCredentials(headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cs.region, err = cs.getRegion(headers)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cs.requestSigner = &awsRequestSigner{
|
||||
RegionName: cs.region,
|
||||
AwsSecurityCredentials: awsSecurityCredentials,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the signed request to AWS STS GetCallerIdentity API.
|
||||
// Use the required regional endpoint. Otherwise, the request will fail.
|
||||
req, err := http.NewRequest("POST", strings.Replace(cs.regionalCredVerificationURL, "{region}", cs.region, 1), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// The full, canonical resource name of the workload identity pool
|
||||
// provider, with or without the HTTPS prefix.
|
||||
// Including this header as part of the signature is recommended to
|
||||
// ensure data integrity.
|
||||
if cs.targetResource != "" {
|
||||
req.Header.Add("x-goog-cloud-target-resource", cs.targetResource)
|
||||
}
|
||||
cs.requestSigner.SignRequest(req)
|
||||
|
||||
/*
|
||||
The GCP STS endpoint expects the headers to be formatted as:
|
||||
# [
|
||||
# {key: 'x-amz-date', value: '...'},
|
||||
# {key: 'Authorization', value: '...'},
|
||||
# ...
|
||||
# ]
|
||||
# And then serialized as:
|
||||
# quote(json.dumps({
|
||||
# url: '...',
|
||||
# method: 'POST',
|
||||
# headers: [{key: 'x-amz-date', value: '...'}, ...]
|
||||
# }))
|
||||
*/
|
||||
|
||||
awsSignedReq := awsRequest{
|
||||
URL: req.URL.String(),
|
||||
Method: "POST",
|
||||
}
|
||||
for headerKey, headerList := range req.Header {
|
||||
for _, headerValue := range headerList {
|
||||
awsSignedReq.Headers = append(awsSignedReq.Headers, awsRequestHeader{
|
||||
Key: headerKey,
|
||||
Value: headerValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(awsSignedReq.Headers, func(i, j int) bool {
|
||||
headerCompare := strings.Compare(awsSignedReq.Headers[i].Key, awsSignedReq.Headers[j].Key)
|
||||
if headerCompare == 0 {
|
||||
return strings.Compare(awsSignedReq.Headers[i].Value, awsSignedReq.Headers[j].Value) < 0
|
||||
}
|
||||
return headerCompare < 0
|
||||
})
|
||||
|
||||
result, err := json.Marshal(awsSignedReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return url.QueryEscape(string(result)), nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
||||
if cs.imdsv2SessionTokenURL == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", cs.imdsv2SessionTokenURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Add(awsIMDSv2SessionTtlHeader, awsIMDSv2SessionTtl)
|
||||
|
||||
resp, err := cs.doRequest(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS session token - %s", string(respBody))
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.ctx, cs.supplierOptions)
|
||||
}
|
||||
if canRetrieveRegionFromEnvironment() {
|
||||
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
|
||||
cs.region = envAwsRegion
|
||||
return envAwsRegion, nil
|
||||
}
|
||||
return getenv("AWS_DEFAULT_REGION"), nil
|
||||
}
|
||||
|
||||
if cs.regionURL == "" {
|
||||
return "", errors.New("oauth2/google/externalaccount: unable to determine AWS region")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", cs.regionURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
|
||||
resp, err := cs.doRequest(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS region - %s", string(respBody))
|
||||
}
|
||||
|
||||
// This endpoint will return the region in format: us-east-2b.
|
||||
// Only the us-east-2 part should be used.
|
||||
respBodyEnd := 0
|
||||
if len(respBody) > 1 {
|
||||
respBodyEnd = len(respBody) - 1
|
||||
}
|
||||
return string(respBody[:respBodyEnd]), nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result *AwsSecurityCredentials, err error) {
|
||||
if cs.awsSecurityCredentialsSupplier != nil {
|
||||
return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.ctx, cs.supplierOptions)
|
||||
}
|
||||
if canRetrieveSecurityCredentialFromEnvironment() {
|
||||
return &AwsSecurityCredentials{
|
||||
AccessKeyID: getenv(awsAccessKeyId),
|
||||
SecretAccessKey: getenv(awsSecretAccessKey),
|
||||
SessionToken: getenv(awsSessionToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
roleName, err := cs.getMetadataRoleName(headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
credentials, err := cs.getMetadataSecurityCredentials(roleName, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if credentials.AccessKeyID == "" {
|
||||
return result, errors.New("oauth2/google/externalaccount: missing AccessKeyId credential")
|
||||
}
|
||||
|
||||
if credentials.SecretAccessKey == "" {
|
||||
return result, errors.New("oauth2/google/externalaccount: missing SecretAccessKey credential")
|
||||
}
|
||||
|
||||
return &credentials, nil
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) {
|
||||
var result AwsSecurityCredentials
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.credVerificationURL, roleName), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
|
||||
resp, err := cs.doRequest(req)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return result, fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS security credentials - %s", string(respBody))
|
||||
}
|
||||
|
||||
err = json.Unmarshal(respBody, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) {
|
||||
if cs.credVerificationURL == "" {
|
||||
return "", errors.New("oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", cs.credVerificationURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
|
||||
resp, err := cs.doRequest(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS role name - %s", string(respBody))
|
||||
}
|
||||
|
||||
return string(respBody), nil
|
||||
}
|
||||
517
vendor/golang.org/x/oauth2/google/externalaccount/basecredentials.go
generated
vendored
Normal file
517
vendor/golang.org/x/oauth2/google/externalaccount/basecredentials.go
generated
vendored
Normal file
@@ -0,0 +1,517 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package externalaccount provides support for creating workload identity
|
||||
federation and workforce identity federation token sources that can be
|
||||
used to access Google Cloud resources from external identity providers.
|
||||
|
||||
# Workload Identity Federation
|
||||
|
||||
Using workload identity federation, your application can access Google Cloud
|
||||
resources from Amazon Web Services (AWS), Microsoft Azure or any identity
|
||||
provider that supports OpenID Connect (OIDC) or SAML 2.0.
|
||||
Traditionally, applications running outside Google Cloud have used service
|
||||
account keys to access Google Cloud resources. Using identity federation,
|
||||
you can allow your workload to impersonate a service account.
|
||||
This lets you access Google Cloud resources directly, eliminating the
|
||||
maintenance and security burden associated with service account keys.
|
||||
|
||||
Follow the detailed instructions on how to configure Workload Identity Federation
|
||||
in various platforms:
|
||||
|
||||
Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws
|
||||
Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure
|
||||
OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc
|
||||
SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml
|
||||
|
||||
For OIDC and SAML providers, the library can retrieve tokens in fours ways:
|
||||
from a local file location (file-sourced credentials), from a server
|
||||
(URL-sourced credentials), from a local executable (executable-sourced
|
||||
credentials), or from a user defined function that returns an OIDC or SAML token.
|
||||
For file-sourced credentials, a background process needs to be continuously
|
||||
refreshing the file location with a new OIDC/SAML token prior to expiration.
|
||||
For tokens with one hour lifetimes, the token needs to be updated in the file
|
||||
every hour. The token can be stored directly as plain text or in JSON format.
|
||||
For URL-sourced credentials, a local server needs to host a GET endpoint to
|
||||
return the OIDC/SAML token. The response can be in plain text or JSON.
|
||||
Additional required request headers can also be specified.
|
||||
For executable-sourced credentials, an application needs to be available to
|
||||
output the OIDC/SAML token and other information in a JSON format.
|
||||
For more information on how these work (and how to implement
|
||||
executable-sourced credentials), please check out:
|
||||
https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration
|
||||
|
||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers,
|
||||
or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config].
|
||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used to access Google
|
||||
Cloud resources. For instance, you can create a new client from the
|
||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
||||
|
||||
Note that this library does not perform any validation on the token_url, token_info_url,
|
||||
or service_account_impersonation_url fields of the credential configuration.
|
||||
It is not recommended to use a credential configuration that you did not generate with
|
||||
the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
|
||||
|
||||
# Workforce Identity Federation
|
||||
|
||||
Workforce identity federation lets you use an external identity provider (IdP) to
|
||||
authenticate and authorize a workforce—a group of users, such as employees, partners,
|
||||
and contractors—using IAM, so that the users can access Google Cloud services.
|
||||
Workforce identity federation extends Google Cloud's identity capabilities to support
|
||||
syncless, attribute-based single sign on.
|
||||
|
||||
With workforce identity federation, your workforce can access Google Cloud resources
|
||||
using an external identity provider (IdP) that supports OpenID Connect (OIDC) or
|
||||
SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation
|
||||
Services (AD FS), Okta, and others.
|
||||
|
||||
Follow the detailed instructions on how to configure Workload Identity Federation
|
||||
in various platforms:
|
||||
|
||||
Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad
|
||||
Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta
|
||||
OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc
|
||||
SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml
|
||||
|
||||
For workforce identity federation, the library can retrieve tokens in four ways:
|
||||
from a local file location (file-sourced credentials), from a server
|
||||
(URL-sourced credentials), from a local executable (executable-sourced
|
||||
credentials), or from a user supplied function that returns an OIDC or SAML token.
|
||||
For file-sourced credentials, a background process needs to be continuously
|
||||
refreshing the file location with a new OIDC/SAML token prior to expiration.
|
||||
For tokens with one hour lifetimes, the token needs to be updated in the file
|
||||
every hour. The token can be stored directly as plain text or in JSON format.
|
||||
For URL-sourced credentials, a local server needs to host a GET endpoint to
|
||||
return the OIDC/SAML token. The response can be in plain text or JSON.
|
||||
Additional required request headers can also be specified.
|
||||
For executable-sourced credentials, an application needs to be available to
|
||||
output the OIDC/SAML token and other information in a JSON format.
|
||||
For more information on how these work (and how to implement
|
||||
executable-sourced credentials), please check out:
|
||||
https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in
|
||||
|
||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers.
|
||||
This can then be used when building a [Config].
|
||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google
|
||||
Cloud resources. For instance, you can create a new client from the
|
||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
||||
|
||||
# Security considerations
|
||||
|
||||
Note that this library does not perform any validation on the token_url, token_info_url,
|
||||
or service_account_impersonation_url fields of the credential configuration.
|
||||
It is not recommended to use a credential configuration that you did not generate with
|
||||
the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
|
||||
*/
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/internal/impersonate"
|
||||
"golang.org/x/oauth2/google/internal/stsexchange"
|
||||
)
|
||||
|
||||
const (
|
||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
||||
defaultUniverseDomain = "googleapis.com"
|
||||
)
|
||||
|
||||
// now aliases time.Now for testing
|
||||
var now = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// Config stores the configuration for fetching tokens with external credentials.
|
||||
type Config struct {
|
||||
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
||||
// identity pool or the workforce pool and the provider identifier in that pool. Required.
|
||||
Audience string
|
||||
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec.
|
||||
// Expected values include:
|
||||
// “urn:ietf:params:oauth:token-type:jwt”
|
||||
// “urn:ietf:params:oauth:token-type:id-token”
|
||||
// “urn:ietf:params:oauth:token-type:saml2”
|
||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
||||
// Required.
|
||||
SubjectTokenType string
|
||||
// TokenURL is the STS token exchange endpoint. If not provided, will default to
|
||||
// https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set to the
|
||||
// default service domain googleapis.com unless UniverseDomain is set.
|
||||
// Optional.
|
||||
TokenURL string
|
||||
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
||||
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
||||
// needed for gCloud session account identification. Optional.
|
||||
TokenInfoURL string
|
||||
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
||||
// required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional.
|
||||
ServiceAccountImpersonationURL string
|
||||
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
||||
// token will be valid for. If not provided, it will default to 3600. Optional.
|
||||
ServiceAccountImpersonationLifetimeSeconds int
|
||||
// ClientSecret is currently only required if token_info endpoint also
|
||||
// needs to be called with the generated GCP access token. When provided, STS will be
|
||||
// called with additional basic authentication using ClientId as username and ClientSecret as password. Optional.
|
||||
ClientSecret string
|
||||
// ClientID is only required in conjunction with ClientSecret, as described above. Optional.
|
||||
ClientID string
|
||||
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
||||
// as some environmental information. One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or
|
||||
// CredentialSource must be provided. Optional.
|
||||
CredentialSource *CredentialSource
|
||||
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
||||
// will set the x-goog-user-project header which overrides the project associated with the credentials. Optional.
|
||||
QuotaProjectID string
|
||||
// Scopes contains the desired scopes for the returned access token. Optional.
|
||||
Scopes []string
|
||||
// WorkforcePoolUserProject is the workforce pool user project number when the credential
|
||||
// corresponds to a workforce pool and not a workload identity pool.
|
||||
// The underlying principal must still have serviceusage.services.use IAM
|
||||
// permission to use the project for billing/quota. Optional.
|
||||
WorkforcePoolUserProject string
|
||||
// SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials.
|
||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
||||
SubjectTokenSupplier SubjectTokenSupplier
|
||||
// AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials.
|
||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
||||
AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
||||
// This value will be used in the default STS token URL. The default value
|
||||
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
|
||||
UniverseDomain string
|
||||
}
|
||||
|
||||
var (
|
||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||
)
|
||||
|
||||
func validateWorkforceAudience(input string) bool {
|
||||
return validWorkforceAudiencePattern.MatchString(input)
|
||||
}
|
||||
|
||||
// NewTokenSource Returns an external account TokenSource using the provided external account config.
|
||||
func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) {
|
||||
if conf.Audience == "" {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Audience must be set")
|
||||
}
|
||||
if conf.SubjectTokenType == "" {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Subject token type must be set")
|
||||
}
|
||||
if conf.WorkforcePoolUserProject != "" {
|
||||
valid := validateWorkforceAudience(conf.Audience)
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials")
|
||||
}
|
||||
}
|
||||
count := 0
|
||||
if conf.CredentialSource != nil {
|
||||
count++
|
||||
}
|
||||
if conf.SubjectTokenSupplier != nil {
|
||||
count++
|
||||
}
|
||||
if conf.AwsSecurityCredentialsSupplier != nil {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
||||
}
|
||||
if count > 1 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
||||
}
|
||||
return conf.tokenSource(ctx, "https")
|
||||
}
|
||||
|
||||
// tokenSource is a private function that's directly called by some of the tests,
|
||||
// because the unit test URLs are mocked, and would otherwise fail the
|
||||
// validity check.
|
||||
func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
|
||||
|
||||
ts := tokenSource{
|
||||
ctx: ctx,
|
||||
conf: c,
|
||||
}
|
||||
if c.ServiceAccountImpersonationURL == "" {
|
||||
return oauth2.ReuseTokenSource(nil, ts), nil
|
||||
}
|
||||
scopes := c.Scopes
|
||||
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||
imp := impersonate.ImpersonateTokenSource{
|
||||
Ctx: ctx,
|
||||
URL: c.ServiceAccountImpersonationURL,
|
||||
Scopes: scopes,
|
||||
Ts: oauth2.ReuseTokenSource(nil, ts),
|
||||
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
||||
}
|
||||
return oauth2.ReuseTokenSource(nil, imp), nil
|
||||
}
|
||||
|
||||
// Subject token file types.
|
||||
const (
|
||||
fileTypeText = "text"
|
||||
fileTypeJSON = "json"
|
||||
)
|
||||
|
||||
// Format contains information needed to retrieve a subject token for URL or File sourced credentials.
|
||||
type Format struct {
|
||||
// Type should be either "text" or "json". This determines whether the file or URL sourced credentials
|
||||
// expect a simple text subject token or if the subject token will be contained in a JSON object.
|
||||
// When not provided "text" type is assumed.
|
||||
Type string `json:"type"`
|
||||
// SubjectTokenFieldName is only required for JSON format. This is the field name that the credentials will check
|
||||
// for the subject token in the file or URL response. This would be "access_token" for azure.
|
||||
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
||||
}
|
||||
|
||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||
type CredentialSource struct {
|
||||
// File is the location for file sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
File string `json:"file"`
|
||||
|
||||
// Url is the URL to call for URL sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
URL string `json:"url"`
|
||||
// Headers are the headers to attach to the request for URL sourced credentials.
|
||||
Headers map[string]string `json:"headers"`
|
||||
|
||||
// Executable is the configuration object for executable sourced credentials.
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
Executable *ExecutableConfig `json:"executable"`
|
||||
|
||||
// EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS".
|
||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
||||
//
|
||||
// Important: If you accept a credential configuration (credential
|
||||
// JSON/File/Stream) from an external source for authentication to Google
|
||||
// Cloud Platform, you must validate it before providing it to any Google
|
||||
// API or library. Providing an unvalidated credential configuration to
|
||||
// Google APIs can compromise the security of your systems and data. For
|
||||
// more information, refer to [Validate credential configurations from
|
||||
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
|
||||
EnvironmentID string `json:"environment_id"`
|
||||
// RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials.
|
||||
RegionURL string `json:"region_url"`
|
||||
// RegionalCredVerificationURL is the AWS regional credential verification URL, will default to
|
||||
// "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided."
|
||||
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
||||
// IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS.
|
||||
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
||||
// Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json".
|
||||
Format Format `json:"format"`
|
||||
}
|
||||
|
||||
// ExecutableConfig contains information needed for executable sourced credentials.
|
||||
type ExecutableConfig struct {
|
||||
// Command is the the full command to run to retrieve the subject token.
|
||||
// This can include arguments. Must be an absolute path for the program. Required.
|
||||
Command string `json:"command"`
|
||||
// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
|
||||
TimeoutMillis *int `json:"timeout_millis"`
|
||||
// OutputFile is the absolute path to the output file where the executable will cache the response.
|
||||
// If specified the auth libraries will first check this location before running the executable. Optional.
|
||||
OutputFile string `json:"output_file"`
|
||||
}
|
||||
|
||||
// SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token.
|
||||
type SubjectTokenSupplier interface {
|
||||
// SubjectToken should return a valid subject token or an error.
|
||||
// The external account token source does not cache the returned subject token, so caching
|
||||
// logic should be implemented in the supplier to prevent multiple requests for the same subject token.
|
||||
SubjectToken(ctx context.Context, options SupplierOptions) (string, error)
|
||||
}
|
||||
|
||||
// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to
|
||||
// exchange for a GCP access token.
|
||||
type AwsSecurityCredentialsSupplier interface {
|
||||
// AwsRegion should return the AWS region or an error.
|
||||
AwsRegion(ctx context.Context, options SupplierOptions) (string, error)
|
||||
// AwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error.
|
||||
// The external account token source does not cache the returned security credentials, so caching
|
||||
// logic should be implemented in the supplier to prevent multiple requests for the same security credentials.
|
||||
AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error)
|
||||
}
|
||||
|
||||
// SupplierOptions contains information about the requested subject token or AWS security credentials from the
|
||||
// Google external account credential.
|
||||
type SupplierOptions struct {
|
||||
// Audience is the requested audience for the external account credential.
|
||||
Audience string
|
||||
// Subject token type is the requested subject token type for the external account credential. Expected values include:
|
||||
// “urn:ietf:params:oauth:token-type:jwt”
|
||||
// “urn:ietf:params:oauth:token-type:id-token”
|
||||
// “urn:ietf:params:oauth:token-type:saml2”
|
||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
||||
SubjectTokenType string
|
||||
}
|
||||
|
||||
// tokenURL returns the default STS token endpoint with the configured universe
|
||||
// domain.
|
||||
func (c *Config) tokenURL() string {
|
||||
if c.UniverseDomain == "" {
|
||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
||||
}
|
||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
|
||||
}
|
||||
|
||||
// parse determines the type of CredentialSource needed.
|
||||
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
||||
//set Defaults
|
||||
if c.TokenURL == "" {
|
||||
c.TokenURL = c.tokenURL()
|
||||
}
|
||||
supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
|
||||
|
||||
if c.AwsSecurityCredentialsSupplier != nil {
|
||||
awsCredSource := awsCredentialSource{
|
||||
awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier,
|
||||
targetResource: c.Audience,
|
||||
supplierOptions: supplierOptions,
|
||||
ctx: ctx,
|
||||
}
|
||||
return awsCredSource, nil
|
||||
} else if c.SubjectTokenSupplier != nil {
|
||||
return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil
|
||||
} else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
||||
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
||||
if awsVersion != 1 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion)
|
||||
}
|
||||
|
||||
awsCredSource := awsCredentialSource{
|
||||
environmentID: c.CredentialSource.EnvironmentID,
|
||||
regionURL: c.CredentialSource.RegionURL,
|
||||
regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
||||
credVerificationURL: c.CredentialSource.URL,
|
||||
targetResource: c.Audience,
|
||||
ctx: ctx,
|
||||
}
|
||||
if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
||||
awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
||||
}
|
||||
|
||||
return awsCredSource, nil
|
||||
}
|
||||
} else if c.CredentialSource.File != "" {
|
||||
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
||||
} else if c.CredentialSource.URL != "" {
|
||||
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
||||
} else if c.CredentialSource.Executable != nil {
|
||||
return createExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
||||
}
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source")
|
||||
}
|
||||
|
||||
type baseCredentialSource interface {
|
||||
credentialSourceType() string
|
||||
subjectToken() (string, error)
|
||||
}
|
||||
|
||||
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
|
||||
type tokenSource struct {
|
||||
ctx context.Context
|
||||
conf *Config
|
||||
}
|
||||
|
||||
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
|
||||
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
|
||||
goVersion(),
|
||||
"unknown",
|
||||
credSource.credentialSourceType(),
|
||||
conf.ServiceAccountImpersonationURL != "",
|
||||
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
|
||||
}
|
||||
|
||||
// Token allows tokenSource to conform to the oauth2.TokenSource interface.
|
||||
func (ts tokenSource) Token() (*oauth2.Token, error) {
|
||||
conf := ts.conf
|
||||
|
||||
credSource, err := conf.parse(ts.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subjectToken, err := credSource.subjectToken()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsRequest := stsexchange.TokenExchangeRequest{
|
||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||
Audience: conf.Audience,
|
||||
Scope: conf.Scopes,
|
||||
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||
SubjectToken: subjectToken,
|
||||
SubjectTokenType: conf.SubjectTokenType,
|
||||
}
|
||||
header := make(http.Header)
|
||||
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
|
||||
clientAuth := stsexchange.ClientAuthentication{
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
ClientID: conf.ClientID,
|
||||
ClientSecret: conf.ClientSecret,
|
||||
}
|
||||
var options map[string]any
|
||||
// Do not pass workforce_pool_user_project when client authentication is used.
|
||||
// The client ID is sufficient for determining the user project.
|
||||
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
||||
options = map[string]any{
|
||||
"userProject": conf.WorkforcePoolUserProject,
|
||||
}
|
||||
}
|
||||
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken := &oauth2.Token{
|
||||
AccessToken: stsResp.AccessToken,
|
||||
TokenType: stsResp.TokenType,
|
||||
}
|
||||
|
||||
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
|
||||
if stsResp.ExpiresIn <= 0 {
|
||||
return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service")
|
||||
}
|
||||
accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
||||
|
||||
if stsResp.RefreshToken != "" {
|
||||
accessToken.RefreshToken = stsResp.RefreshToken
|
||||
}
|
||||
return accessToken, nil
|
||||
}
|
||||
312
vendor/golang.org/x/oauth2/google/externalaccount/executablecredsource.go
generated
vendored
Normal file
312
vendor/golang.org/x/oauth2/google/externalaccount/executablecredsource.go
generated
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials\\..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
||||
|
||||
const (
|
||||
executableSupportedMaxVersion = 1
|
||||
defaultTimeout = 30 * time.Second
|
||||
timeoutMinimum = 5 * time.Second
|
||||
timeoutMaximum = 120 * time.Second
|
||||
executableSource = "response"
|
||||
outputFileSource = "output file"
|
||||
)
|
||||
|
||||
type nonCacheableError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (nce nonCacheableError) Error() string {
|
||||
return nce.message
|
||||
}
|
||||
|
||||
func missingFieldError(source, field string) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v missing `%q` field", source, field)
|
||||
}
|
||||
|
||||
func jsonParsingError(source, data string) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: unable to parse %v\nResponse: %v", source, data)
|
||||
}
|
||||
|
||||
func malformedFailureError() error {
|
||||
return nonCacheableError{"oauth2/google/externalaccount: response must include `error` and `message` fields when unsuccessful"}
|
||||
}
|
||||
|
||||
func userDefinedError(code, message string) error {
|
||||
return nonCacheableError{fmt.Sprintf("oauth2/google/externalaccount: response contains unsuccessful response: (%v) %v", code, message)}
|
||||
}
|
||||
|
||||
func unsupportedVersionError(source string, version int) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported version: %v", source, version)
|
||||
}
|
||||
|
||||
func tokenExpiredError() error {
|
||||
return nonCacheableError{"oauth2/google/externalaccount: the token returned by the executable is expired"}
|
||||
}
|
||||
|
||||
func tokenTypeError(source string) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported token type", source)
|
||||
}
|
||||
|
||||
func exitCodeError(exitCode int) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed with exit code %v", exitCode)
|
||||
}
|
||||
|
||||
func executableError(err error) error {
|
||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed: %v", err)
|
||||
}
|
||||
|
||||
func executablesDisallowedError() error {
|
||||
return errors.New("oauth2/google/externalaccount: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
||||
}
|
||||
|
||||
func timeoutRangeError() error {
|
||||
return errors.New("oauth2/google/externalaccount: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
||||
}
|
||||
|
||||
func commandMissingError() error {
|
||||
return errors.New("oauth2/google/externalaccount: missing `command` field — executable command must be provided")
|
||||
}
|
||||
|
||||
type environment interface {
|
||||
existingEnv() []string
|
||||
getenv(string) string
|
||||
run(ctx context.Context, command string, env []string) ([]byte, error)
|
||||
now() time.Time
|
||||
}
|
||||
|
||||
type runtimeEnvironment struct{}
|
||||
|
||||
func (r runtimeEnvironment) existingEnv() []string {
|
||||
return os.Environ()
|
||||
}
|
||||
|
||||
func (r runtimeEnvironment) getenv(key string) string {
|
||||
return os.Getenv(key)
|
||||
}
|
||||
|
||||
func (r runtimeEnvironment) now() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
|
||||
splitCommand := strings.Fields(command)
|
||||
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
|
||||
cmd.Env = env
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
return nil, exitCodeError(exitError.ExitCode())
|
||||
}
|
||||
|
||||
return nil, executableError(err)
|
||||
}
|
||||
|
||||
bytesStdout := bytes.TrimSpace(stdout.Bytes())
|
||||
if len(bytesStdout) > 0 {
|
||||
return bytesStdout, nil
|
||||
}
|
||||
return bytes.TrimSpace(stderr.Bytes()), nil
|
||||
}
|
||||
|
||||
type executableCredentialSource struct {
|
||||
Command string
|
||||
Timeout time.Duration
|
||||
OutputFile string
|
||||
ctx context.Context
|
||||
config *Config
|
||||
env environment
|
||||
}
|
||||
|
||||
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
||||
// It also performs defaulting and type conversions.
|
||||
func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
||||
if ec.Command == "" {
|
||||
return executableCredentialSource{}, commandMissingError()
|
||||
}
|
||||
|
||||
result := executableCredentialSource{}
|
||||
result.Command = ec.Command
|
||||
if ec.TimeoutMillis == nil {
|
||||
result.Timeout = defaultTimeout
|
||||
} else {
|
||||
result.Timeout = time.Duration(*ec.TimeoutMillis) * time.Millisecond
|
||||
if result.Timeout < timeoutMinimum || result.Timeout > timeoutMaximum {
|
||||
return executableCredentialSource{}, timeoutRangeError()
|
||||
}
|
||||
}
|
||||
result.OutputFile = ec.OutputFile
|
||||
result.ctx = ctx
|
||||
result.config = config
|
||||
result.env = runtimeEnvironment{}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type executableResponse struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
Success *bool `json:"success,omitempty"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
ExpirationTime int64 `json:"expiration_time,omitempty"`
|
||||
IdToken string `json:"id_token,omitempty"`
|
||||
SamlResponse string `json:"saml_response,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
|
||||
var result executableResponse
|
||||
if err := json.Unmarshal(response, &result); err != nil {
|
||||
return "", jsonParsingError(source, string(response))
|
||||
}
|
||||
|
||||
if result.Version == 0 {
|
||||
return "", missingFieldError(source, "version")
|
||||
}
|
||||
|
||||
if result.Success == nil {
|
||||
return "", missingFieldError(source, "success")
|
||||
}
|
||||
|
||||
if !*result.Success {
|
||||
if result.Code == "" || result.Message == "" {
|
||||
return "", malformedFailureError()
|
||||
}
|
||||
return "", userDefinedError(result.Code, result.Message)
|
||||
}
|
||||
|
||||
if result.Version > executableSupportedMaxVersion || result.Version < 0 {
|
||||
return "", unsupportedVersionError(source, result.Version)
|
||||
}
|
||||
|
||||
if result.ExpirationTime == 0 && cs.OutputFile != "" {
|
||||
return "", missingFieldError(source, "expiration_time")
|
||||
}
|
||||
|
||||
if result.TokenType == "" {
|
||||
return "", missingFieldError(source, "token_type")
|
||||
}
|
||||
|
||||
if result.ExpirationTime != 0 && result.ExpirationTime < now {
|
||||
return "", tokenExpiredError()
|
||||
}
|
||||
|
||||
if result.TokenType == "urn:ietf:params:oauth:token-type:jwt" || result.TokenType == "urn:ietf:params:oauth:token-type:id_token" {
|
||||
if result.IdToken == "" {
|
||||
return "", missingFieldError(source, "id_token")
|
||||
}
|
||||
return result.IdToken, nil
|
||||
}
|
||||
|
||||
if result.TokenType == "urn:ietf:params:oauth:token-type:saml2" {
|
||||
if result.SamlResponse == "" {
|
||||
return "", missingFieldError(source, "saml_response")
|
||||
}
|
||||
return result.SamlResponse, nil
|
||||
}
|
||||
|
||||
return "", tokenTypeError(source)
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) credentialSourceType() string {
|
||||
return "executable"
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) subjectToken() (string, error) {
|
||||
if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
return cs.getTokenFromExecutableCommand()
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) getTokenFromOutputFile() (token string, err error) {
|
||||
if cs.OutputFile == "" {
|
||||
// This ExecutableCredentialSource doesn't use an OutputFile.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
file, err := os.Open(cs.OutputFile)
|
||||
if err != nil {
|
||||
// No OutputFile found. Hasn't been created yet, so skip it.
|
||||
return "", nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
|
||||
if err != nil || len(data) == 0 {
|
||||
// Cachefile exists, but no data found. Get new credential.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
token, err = cs.parseSubjectTokenFromSource(data, outputFileSource, cs.env.now().Unix())
|
||||
if err != nil {
|
||||
if _, ok := err.(nonCacheableError); ok {
|
||||
// If the cached token is expired we need a new token,
|
||||
// and if the cache contains a failure, we need to try again.
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// There was an error in the cached token, and the developer should be aware of it.
|
||||
return "", err
|
||||
}
|
||||
// Token parsing succeeded. Use found token.
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) executableEnvironment() []string {
|
||||
result := cs.env.existingEnv()
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", cs.config.Audience))
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", cs.config.SubjectTokenType))
|
||||
result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
|
||||
if cs.config.ServiceAccountImpersonationURL != "" {
|
||||
matches := serviceAccountImpersonationRE.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL)
|
||||
if matches != nil {
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
|
||||
}
|
||||
}
|
||||
if cs.OutputFile != "" {
|
||||
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", cs.OutputFile))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, error) {
|
||||
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
|
||||
if cs.env.getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" {
|
||||
return "", executablesDisallowedError()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithDeadline(cs.ctx, cs.env.now().Add(cs.Timeout))
|
||||
defer cancel()
|
||||
|
||||
output, err := cs.env.run(ctx, cs.Command, cs.executableEnvironment())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cs.parseSubjectTokenFromSource(output, executableSource, cs.env.now().Unix())
|
||||
}
|
||||
60
vendor/golang.org/x/oauth2/google/externalaccount/filecredsource.go
generated
vendored
Normal file
60
vendor/golang.org/x/oauth2/google/externalaccount/filecredsource.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type fileCredentialSource struct {
|
||||
File string
|
||||
Format Format
|
||||
}
|
||||
|
||||
func (cs fileCredentialSource) credentialSourceType() string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func (cs fileCredentialSource) subjectToken() (string, error) {
|
||||
tokenFile, err := os.Open(cs.File)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to open credential file %q", cs.File)
|
||||
}
|
||||
defer tokenFile.Close()
|
||||
tokenBytes, err := io.ReadAll(io.LimitReader(tokenFile, 1<<20))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to read credential file: %v", err)
|
||||
}
|
||||
tokenBytes = bytes.TrimSpace(tokenBytes)
|
||||
switch cs.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]any)
|
||||
err = json.Unmarshal(tokenBytes, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
||||
}
|
||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case "text":
|
||||
return string(tokenBytes), nil
|
||||
case "":
|
||||
return string(tokenBytes), nil
|
||||
default:
|
||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
||||
}
|
||||
|
||||
}
|
||||
64
vendor/golang.org/x/oauth2/google/externalaccount/header.go
generated
vendored
Normal file
64
vendor/golang.org/x/oauth2/google/externalaccount/header.go
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
// version is a package internal global variable for testing purposes.
|
||||
version = runtime.Version
|
||||
)
|
||||
|
||||
// versionUnknown is only used when the runtime version cannot be determined.
|
||||
const versionUnknown = "UNKNOWN"
|
||||
|
||||
// goVersion returns a Go runtime version derived from the runtime environment
|
||||
// that is modified to be suitable for reporting in a header, meaning it has no
|
||||
// whitespace. If it is unable to determine the Go runtime version, it returns
|
||||
// versionUnknown.
|
||||
func goVersion() string {
|
||||
const develPrefix = "devel +"
|
||||
|
||||
s := version()
|
||||
if strings.HasPrefix(s, develPrefix) {
|
||||
s = s[len(develPrefix):]
|
||||
if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||
s = s[:p]
|
||||
}
|
||||
return s
|
||||
} else if p := strings.IndexFunc(s, unicode.IsSpace); p >= 0 {
|
||||
s = s[:p]
|
||||
}
|
||||
|
||||
notSemverRune := func(r rune) bool {
|
||||
return !strings.ContainsRune("0123456789.", r)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, "go1") {
|
||||
s = s[2:]
|
||||
var prerelease string
|
||||
if p := strings.IndexFunc(s, notSemverRune); p >= 0 {
|
||||
s, prerelease = s[:p], s[p:]
|
||||
}
|
||||
if strings.HasSuffix(s, ".") {
|
||||
s += "0"
|
||||
} else if strings.Count(s, ".") < 2 {
|
||||
s += ".0"
|
||||
}
|
||||
if prerelease != "" {
|
||||
// Some release candidates already have a dash in them.
|
||||
if !strings.HasPrefix(prerelease, "-") {
|
||||
prerelease = "-" + prerelease
|
||||
}
|
||||
s += prerelease
|
||||
}
|
||||
return s
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
21
vendor/golang.org/x/oauth2/google/externalaccount/programmaticrefreshcredsource.go
generated
vendored
Normal file
21
vendor/golang.org/x/oauth2/google/externalaccount/programmaticrefreshcredsource.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import "context"
|
||||
|
||||
type programmaticRefreshCredentialSource struct {
|
||||
supplierOptions SupplierOptions
|
||||
subjectTokenSupplier SubjectTokenSupplier
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (cs programmaticRefreshCredentialSource) credentialSourceType() string {
|
||||
return "programmatic"
|
||||
}
|
||||
|
||||
func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) {
|
||||
return cs.subjectTokenSupplier.SubjectToken(cs.ctx, cs.supplierOptions)
|
||||
}
|
||||
78
vendor/golang.org/x/oauth2/google/externalaccount/urlcredsource.go
generated
vendored
Normal file
78
vendor/golang.org/x/oauth2/google/externalaccount/urlcredsource.go
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type urlCredentialSource struct {
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Format Format
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (cs urlCredentialSource) credentialSourceType() string {
|
||||
return "url"
|
||||
}
|
||||
|
||||
func (cs urlCredentialSource) subjectToken() (string, error) {
|
||||
client := oauth2.NewClient(cs.ctx, nil)
|
||||
req, err := http.NewRequest("GET", cs.URL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: HTTP request for URL-sourced credential failed: %v", err)
|
||||
}
|
||||
req = req.WithContext(cs.ctx)
|
||||
|
||||
for key, val := range cs.Headers {
|
||||
req.Header.Add(key, val)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid response when retrieving subject token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid body in subject token URL query: %v", err)
|
||||
}
|
||||
if c := resp.StatusCode; c < 200 || c > 299 {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: status code %d: %s", c, respBody)
|
||||
}
|
||||
|
||||
switch cs.Format.Type {
|
||||
case "json":
|
||||
jsonData := make(map[string]any)
|
||||
err = json.Unmarshal(respBody, &jsonData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
||||
}
|
||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
||||
}
|
||||
token, ok := val.(string)
|
||||
if !ok {
|
||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
||||
}
|
||||
return token, nil
|
||||
case "text":
|
||||
return string(respBody), nil
|
||||
case "":
|
||||
return string(respBody), nil
|
||||
default:
|
||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user