forked from googleapis/google-api-go-client
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: allow impersonated_service_account credential type to generate …
…identity token on the current impersonated service account
- Loading branch information
1 parent
2560eda
commit fda4192
Showing
3 changed files
with
163 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Copyright 2020 Google LLC. | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package idtoken | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"golang.org/x/oauth2/google" | ||
"io/ioutil" | ||
"net/http" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"golang.org/x/oauth2" | ||
|
||
"google.golang.org/api/internal" | ||
) | ||
|
||
var impersonateUrlFormat = regexp.MustCompile(`https:\/\/iamcredentials.googleapis.com\/v1\/projects\/-\/serviceAccounts\/\S+@\S+\.iam\.gserviceaccount\.com:generateAccessToken`) | ||
|
||
// impersonateServiceAccountTokenSource uses impersonated service account credential type. It will | ||
// use the Service Account Credential API and the generateIdToken method to fetch identity token on | ||
// the current impersonated service account. | ||
func impersonateServiceAccountTokenSource(audience string, ds *internal.DialSettings, creds *google.Credentials) (oauth2.TokenSource, error) { | ||
if ds.CustomClaims != nil { | ||
return nil, fmt.Errorf("idtoken: WithCustomClaims can't be used with impersonated credentials, please provide a service account if you would like to use this feature") | ||
} | ||
var f struct { | ||
ImpersonationUrl string `json:"service_account_impersonation_url"` | ||
Delegates []string `json:"delegates"` | ||
} | ||
if err := json.Unmarshal(creds.JSON, &f); err != nil { | ||
return nil, err | ||
} | ||
|
||
// Check URL value and format | ||
if f.ImpersonationUrl == "" { | ||
return nil, fmt.Errorf("idtoken: service_account_impersonation_url can't be empty for credential type impersonated_service_account") | ||
} | ||
if !impersonateUrlFormat.Match([]byte(f.ImpersonationUrl)) { | ||
return nil, fmt.Errorf("idtoken: service_account_impersonation_url doesn't have the correct pattern") | ||
} | ||
|
||
serviceAccountEmail := f.ImpersonationUrl[strings.LastIndex(f.ImpersonationUrl, "/")+1 : strings.LastIndex(f.ImpersonationUrl, ":")] | ||
idTokenUrl := strings.Replace(f.ImpersonationUrl, "generateAccessToken", "generateIdToken", 1) | ||
|
||
ts := impersonateServiceAccount{ | ||
audience: audience, | ||
email: serviceAccountEmail, | ||
idTokenUrl: idTokenUrl, | ||
delegates: f.Delegates, | ||
// Reuse current impersonate service account definition to generate an access token on a | ||
// standard google OAuth2 HTTP client | ||
client: oauth2.NewClient(context.Background(), creds.TokenSource), | ||
} | ||
tok, err := ts.Token() | ||
if err != nil { | ||
return nil, err | ||
} | ||
return oauth2.ReuseTokenSource(tok, ts), nil | ||
} | ||
|
||
// impersonateServiceAccount represents the minimal data to request an identity token to the | ||
// Service Account Credentials API. | ||
type impersonateServiceAccount struct { | ||
audience string | ||
email string | ||
idTokenUrl string | ||
delegates []string | ||
client *http.Client | ||
} | ||
|
||
// requestGenerateIdToken represents the body to submit to Service Account Credentials API | ||
// to generate an identity token | ||
type requestGenerateIdToken struct { | ||
Delegates []string `json:"delegates"` | ||
Audience string `json:"audience"` | ||
IncludeEmail bool `json:"includeEmail"` | ||
} | ||
|
||
func (c impersonateServiceAccount) Token() (*oauth2.Token, error) { | ||
request := requestGenerateIdToken{ | ||
Delegates: c.delegates, | ||
Audience: c.audience, | ||
IncludeEmail: true, | ||
} | ||
|
||
reqBody, _ := json.Marshal(request) | ||
|
||
resp, err := c.client.Post(c.idTokenUrl, "application/json", bytes.NewBuffer(reqBody)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return nil, errors.New(fmt.Sprintf("idToken: invalid status code %s", resp.Status)) | ||
} | ||
|
||
body, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var f struct { | ||
Token string `json:"token"` | ||
} | ||
|
||
if err = json.Unmarshal(body, &f); err != nil { | ||
return nil, err | ||
} | ||
|
||
if f.Token == "" { | ||
return nil, fmt.Errorf("idtoken: invalid response from Service Account Credential API") | ||
} | ||
return &oauth2.Token{ | ||
AccessToken: f.Token, | ||
TokenType: "bearer", | ||
// Tokens are valid for one hour, leave a little buffer | ||
Expiry: time.Now().Add(55 * time.Minute), | ||
}, nil | ||
} |
credential must be service_account
->credential must be service_account or impersonated_service_account