...

Source file src/cloud.google.com/go/auth/credentials/impersonate/idtoken.go

Documentation: cloud.google.com/go/auth/credentials/impersonate

     1  // Copyright 2023 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package impersonate
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"net/http"
    24  	"time"
    25  
    26  	"cloud.google.com/go/auth"
    27  	"cloud.google.com/go/auth/credentials"
    28  	"cloud.google.com/go/auth/httptransport"
    29  	"cloud.google.com/go/auth/internal"
    30  )
    31  
    32  // IDTokenOptions for generating an impersonated ID token.
    33  type IDTokenOptions struct {
    34  	// Audience is the `aud` field for the token, such as an API endpoint the
    35  	// token will grant access to. Required.
    36  	Audience string
    37  	// TargetPrincipal is the email address of the service account to
    38  	// impersonate. Required.
    39  	TargetPrincipal string
    40  	// IncludeEmail includes the target service account's email in the token.
    41  	// The resulting token will include both an `email` and `email_verified`
    42  	// claim. Optional.
    43  	IncludeEmail bool
    44  	// Delegates are the ordered service account email addresses in a delegation
    45  	// chain. Each service account must be granted
    46  	// roles/iam.serviceAccountTokenCreator on the next service account in the
    47  	// chain. Optional.
    48  	Delegates []string
    49  
    50  	// Credentials used to fetch the ID token. If not provided, and a Client is
    51  	// also not provided, base credentials will try to be detected from the
    52  	// environment. Optional.
    53  	Credentials *auth.Credentials
    54  	// Client configures the underlying client used to make network requests
    55  	// when fetching tokens. If provided the client should provide it's own
    56  	// base credentials at call time. Optional.
    57  	Client *http.Client
    58  }
    59  
    60  func (o *IDTokenOptions) validate() error {
    61  	if o == nil {
    62  		return errors.New("impersonate: options must be provided")
    63  	}
    64  	if o.Audience == "" {
    65  		return errors.New("impersonate: audience must be provided")
    66  	}
    67  	if o.TargetPrincipal == "" {
    68  		return errors.New("impersonate: target service account must be provided")
    69  	}
    70  	return nil
    71  }
    72  
    73  var (
    74  	defaultScope = "https://www.googleapis.com/auth/cloud-platform"
    75  )
    76  
    77  // NewIDTokenCredentials creates an impersonated
    78  // [cloud.google.com/go/auth/Credentials] that returns ID tokens configured
    79  // with the provided config and using credentials loaded from Application
    80  // Default Credentials as the base credentials if not provided with the opts.
    81  // The tokens produced are valid for one hour and are automatically refreshed.
    82  func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) {
    83  	if err := opts.validate(); err != nil {
    84  		return nil, err
    85  	}
    86  	var client *http.Client
    87  	var creds *auth.Credentials
    88  	if opts.Client == nil && opts.Credentials == nil {
    89  		var err error
    90  		// TODO: test not signed jwt more
    91  		creds, err = credentials.DetectDefault(&credentials.DetectOptions{
    92  			Scopes:           []string{defaultScope},
    93  			UseSelfSignedJWT: true,
    94  		})
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		client, err = httptransport.NewClient(&httptransport.Options{
    99  			Credentials: creds,
   100  		})
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  	} else if opts.Client == nil {
   105  		creds = opts.Credentials
   106  		client = internal.CloneDefaultClient()
   107  		if err := httptransport.AddAuthorizationMiddleware(client, opts.Credentials); err != nil {
   108  			return nil, err
   109  		}
   110  	} else {
   111  		client = opts.Client
   112  	}
   113  
   114  	itp := impersonatedIDTokenProvider{
   115  		client:          client,
   116  		targetPrincipal: opts.TargetPrincipal,
   117  		audience:        opts.Audience,
   118  		includeEmail:    opts.IncludeEmail,
   119  	}
   120  	for _, v := range opts.Delegates {
   121  		itp.delegates = append(itp.delegates, formatIAMServiceAccountName(v))
   122  	}
   123  
   124  	var udp auth.CredentialsPropertyProvider
   125  	if creds != nil {
   126  		udp = auth.CredentialsPropertyFunc(creds.UniverseDomain)
   127  	}
   128  	return auth.NewCredentials(&auth.CredentialsOptions{
   129  		TokenProvider:          auth.NewCachedTokenProvider(itp, nil),
   130  		UniverseDomainProvider: udp,
   131  	}), nil
   132  }
   133  
   134  type generateIDTokenRequest struct {
   135  	Audience     string   `json:"audience"`
   136  	IncludeEmail bool     `json:"includeEmail"`
   137  	Delegates    []string `json:"delegates,omitempty"`
   138  }
   139  
   140  type generateIDTokenResponse struct {
   141  	Token string `json:"token"`
   142  }
   143  
   144  type impersonatedIDTokenProvider struct {
   145  	client *http.Client
   146  
   147  	targetPrincipal string
   148  	audience        string
   149  	includeEmail    bool
   150  	delegates       []string
   151  }
   152  
   153  func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
   154  	genIDTokenReq := generateIDTokenRequest{
   155  		Audience:     i.audience,
   156  		IncludeEmail: i.includeEmail,
   157  		Delegates:    i.delegates,
   158  	}
   159  	bodyBytes, err := json.Marshal(genIDTokenReq)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
   162  	}
   163  
   164  	url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal))
   165  	req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
   166  	if err != nil {
   167  		return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
   168  	}
   169  	req.Header.Set("Content-Type", "application/json")
   170  	resp, err := i.client.Do(req)
   171  	if err != nil {
   172  		return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err)
   173  	}
   174  	defer resp.Body.Close()
   175  	body, err := internal.ReadAll(resp.Body)
   176  	if err != nil {
   177  		return nil, fmt.Errorf("impersonate: unable to read body: %w", err)
   178  	}
   179  	if c := resp.StatusCode; c < 200 || c > 299 {
   180  		return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
   181  	}
   182  
   183  	var generateIDTokenResp generateIDTokenResponse
   184  	if err := json.Unmarshal(body, &generateIDTokenResp); err != nil {
   185  		return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
   186  	}
   187  	return &auth.Token{
   188  		Value: generateIDTokenResp.Token,
   189  		// Generated ID tokens are good for one hour.
   190  		Expiry: time.Now().Add(1 * time.Hour),
   191  	}, nil
   192  }
   193  

View as plain text