Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(idtoken): add support for impersonated_service_account creds type #1792

Merged
merged 6 commits into from
Jan 4, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 74 additions & 25 deletions idtoken/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"

"cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"

"google.golang.org/api/impersonate"
"google.golang.org/api/internal"
"google.golang.org/api/option"
"google.golang.org/api/option/internaloption"
Expand All @@ -25,6 +28,14 @@ import (
// ClientOption is for configuring a Google API client or transport.
type ClientOption = option.ClientOption

type credentialsType int

const (
unknownCredType credentialsType = iota
serviceAccount
impersonatedServiceAccount
)

// NewClient creates a HTTP Client that automatically adds an ID token to each
// request via an Authorization header. The token will have the audience
// provided and be configured with the supplied options. The parameter audience
Expand Down Expand Up @@ -103,45 +114,83 @@ func newTokenSource(ctx context.Context, audience string, ds *internal.DialSetti
}

func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
if err := isServiceAccount(data); err != nil {
return nil, err
}
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
allowedType, err := getAllowedType(data)
if err != nil {
return nil, err
}

customClaims := ds.CustomClaims
if customClaims == nil {
customClaims = make(map[string]interface{})
switch allowedType {
case serviceAccount:
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
if err != nil {
return nil, err
}
customClaims := ds.CustomClaims
if customClaims == nil {
customClaims = make(map[string]interface{})
}
customClaims["target_audience"] = audience

cfg.PrivateClaims = customClaims
cfg.UseIDToken = true

ts := cfg.TokenSource(ctx)
tok, err := ts.Token()
if err != nil {
return nil, err
}
return oauth2.ReuseTokenSource(tok, ts), nil
case impersonatedServiceAccount:
type url struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
}
var accountURL *url
if err := json.Unmarshal(data, &accountURL); err != nil {
return nil, err
}
account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
account = strings.Split(account, ":")[0]

config := impersonate.IDTokenConfig{
Audience: audience,
TargetPrincipal: account,
IncludeEmail: true,
}
ts, err := impersonate.IDTokenSource(ctx, config)
if err != nil {
return nil, err
}
return ts, nil
default:
return nil, fmt.Errorf("idtoken: unsupported credentials type")
}
customClaims["target_audience"] = audience

cfg.PrivateClaims = customClaims
cfg.UseIDToken = true

ts := cfg.TokenSource(ctx)
tok, err := ts.Token()
if err != nil {
return nil, err
}
return oauth2.ReuseTokenSource(tok, ts), nil
}

func isServiceAccount(data []byte) error {
// getAllowedType returns the credentials type of type credentialsType, and an error.
// allowed types are "service_account" and "impersonated_service_account"
func getAllowedType(data []byte) (credentialsType, error) {
var t credentialsType
if len(data) == 0 {
return fmt.Errorf("idtoken: credential provided is 0 bytes")
return t, fmt.Errorf("idtoken: credential provided is 0 bytes")
}
var f struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &f); err != nil {
return err
return t, err
}
if f.Type != "service_account" {
return fmt.Errorf("idtoken: credential must be service_account, found %q", f.Type)
t = parseCredType(f.Type)
return t, nil
}

func parseCredType(typeString string) credentialsType {
switch typeString {
case "service_account":
return serviceAccount
case "impersonated_service_account":
return impersonatedServiceAccount
default:
return unknownCredType
}
return nil
}

// WithCustomClaims optionally specifies custom private claims for an ID token.
Expand Down