Skip to content

Commit

Permalink
feat(auth): add universe domain support to idtoken
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo committed Oct 30, 2024
1 parent 2a667c6 commit 260aaaa
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 33 deletions.
109 changes: 86 additions & 23 deletions auth/credentials/idtoken/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ package idtoken
import (
"encoding/json"
"fmt"
"net/http"
"path/filepath"
"strings"

"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/impersonate"
"cloud.google.com/go/auth/httptransport"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
)
Expand Down Expand Up @@ -50,38 +52,23 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) {
if err != nil {
return nil, err
}
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
TokenURL: f.TokenURL,
UseIDToken: true,
var tp auth.TokenProvider
if opts.UseIAMEndpoint {
tp, err = newIAMIDTokenProvider(b, f, opts)
} else {
tp, err = new2LOTokenProvider(f, opts)
}
if opts2LO.TokenURL == "" {
opts2LO.TokenURL = jwtTokenURL
}

var customClaims map[string]interface{}
if opts != nil {
customClaims = opts.CustomClaims
}
if customClaims == nil {
customClaims = make(map[string]interface{})
}
customClaims["target_audience"] = opts.Audience

opts2LO.PrivateClaims = customClaims
tp, err := auth.New2LOTokenProvider(opts2LO)
if err != nil {
return nil, err
}
tp = auth.NewCachedTokenProvider(tp, nil)
return auth.NewCredentials(&auth.CredentialsOptions{
creds := auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: tp,
JSON: b,
ProjectIDProvider: internal.StaticCredentialsProperty(f.ProjectID),
UniverseDomainProvider: internal.StaticCredentialsProperty(f.UniverseDomain),
}), nil
})
return creds, nil
case credsfile.ImpersonatedServiceAccountKey, credsfile.ExternalAccountKey:
type url struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Expand Down Expand Up @@ -125,3 +112,79 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) {
return nil, fmt.Errorf("idtoken: unsupported credentials type: %v", t)
}
}

func new2LOTokenProvider(f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
opts2LO := &auth.Options2LO{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
TokenURL: f.TokenURL,
UseIDToken: true,
}
if opts2LO.TokenURL == "" {
opts2LO.TokenURL = jwtTokenURL
}

var customClaims map[string]interface{}
if opts != nil {
customClaims = opts.CustomClaims
}
if customClaims == nil {
customClaims = make(map[string]interface{})
}
customClaims["target_audience"] = opts.Audience

opts2LO.PrivateClaims = customClaims
return auth.New2LOTokenProvider(opts2LO)
}

// newIAMIDTokenProvider creates a TokenProvider that performs an authenticated
// RPC with the IAM service to obtain an ID token. The provided service account
// must have the iam.serviceAccountTokenCreator role. If a fully-authenticated
// client is not provided, the service account must support a self-signed JWT.
// This TokenProvider is primarily intended for use in non-GDU universes, which
// do not have access to the oauth2.googleapis.com/token endpoint, and thus must
// use IAM generateIdToken instead.
func newIAMIDTokenProvider(b []byte, f *credsfile.ServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
var client *http.Client
var creds *auth.Credentials
var err error
universeDomain := resolveUniverseDomain(f)
if opts.Client == nil {
creds, err = credentials.DetectDefault(&credentials.DetectOptions{
CredentialsJSON: b,
Scopes: []string{"https://www.googleapis.com/auth/iam"},
UseSelfSignedJWT: true,
UniverseDomain: universeDomain,
})
if err != nil {
return nil, err
}
client, err = httptransport.NewClient(&httptransport.Options{
Credentials: creds,
UniverseDomain: universeDomain,
})
if err != nil {
return nil, err
}
} else {
client = opts.Client
}
its := iamIDTokenProvider{
client: client,
universeDomain: universeDomain,
signerEmail: f.ClientEmail,
audience: opts.Audience,
}
return its, nil
}

// resolveUniverseDomain returns the default service domain for a given
// Cloud universe. This is the universe domain configured for the credentials,
// which will be used in endpoint.
func resolveUniverseDomain(f *credsfile.ServiceAccountFile) string {
if f.UniverseDomain != "" {
return f.UniverseDomain
}
return internal.DefaultUniverseDomain
}
5 changes: 5 additions & 0 deletions auth/credentials/idtoken/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ type Options struct {
// when fetching tokens. If provided this should be a fully authenticated
// client. Optional.
Client *http.Client
// UseIAMEndpoint configures whether the IAM generateIdToken endpoint will
// be used instead of the oauth2.googleapis.com/token endpoint. Note that
// the iam.serviceAccountTokenCreator role is required to use the IAM
// generateIdToken endpoint. The default value is false. Optional.
UseIAMEndpoint bool
}

func (o *Options) client() *http.Client {
Expand Down
4 changes: 2 additions & 2 deletions auth/credentials/impersonate/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) {
includeEmail: opts.IncludeEmail,
}
for _, v := range opts.Delegates {
itp.delegates = append(itp.delegates, formatIAMServiceAccountName(v))
itp.delegates = append(itp.delegates, internal.FormatIAMServiceAccountName(v))
}

var udp auth.CredentialsPropertyProvider
Expand Down Expand Up @@ -161,7 +161,7 @@ func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, er
return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
}

url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal))
url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal))
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
Expand Down
8 changes: 2 additions & 6 deletions auth/credentials/impersonate/impersonate.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func NewCredentials(opts *CredentialsOptions) (*auth.Credentials, error) {
universeDomainProvider: universeDomainProvider,
}
for _, v := range opts.Delegates {
its.delegates = append(its.delegates, formatIAMServiceAccountName(v))
its.delegates = append(its.delegates, internal.FormatIAMServiceAccountName(v))
}
its.scopes = make([]string, len(opts.Scopes))
copy(its.scopes, opts.Scopes)
Expand Down Expand Up @@ -197,10 +197,6 @@ func (o *CredentialsOptions) validate() error {
return nil
}

func formatIAMServiceAccountName(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}

type generateAccessTokenRequest struct {
Delegates []string `json:"delegates,omitempty"`
Lifetime string `json:"lifetime,omitempty"`
Expand Down Expand Up @@ -238,7 +234,7 @@ func (i impersonatedTokenProvider) Token(ctx context.Context) (*auth.Token, erro
return nil, err
}
endpoint := strings.Replace(iamCredentialsUniverseDomainEndpoint, universeDomainPlaceholder, universeDomain, 1)
url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, formatIAMServiceAccountName(i.targetPrincipal))
url := fmt.Sprintf("%s/v1/%s:generateAccessToken", endpoint, internal.FormatIAMServiceAccountName(i.targetPrincipal))
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
if err != nil {
return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions auth/credentials/impersonate/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func user(opts *CredentialsOptions, client *http.Client, lifetime time.Duration,
}
u.delegates = make([]string, len(opts.Delegates))
for i, v := range opts.Delegates {
u.delegates[i] = formatIAMServiceAccountName(v)
u.delegates[i] = internal.FormatIAMServiceAccountName(v)
}
u.scopes = make([]string, len(opts.Scopes))
copy(u.scopes, opts.Scopes)
Expand Down Expand Up @@ -139,7 +139,7 @@ func (u userTokenProvider) signJWT(ctx context.Context) (string, error) {
if err != nil {
return "", fmt.Errorf("impersonate: unable to marshal request: %w", err)
}
reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, formatIAMServiceAccountName(u.targetPrincipal))
reqURL := fmt.Sprintf("%s/v1/%s:signJwt", iamCredentialsEndpoint, internal.FormatIAMServiceAccountName(u.targetPrincipal))
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("impersonate: unable to create request: %w", err)
Expand Down
6 changes: 6 additions & 0 deletions auth/internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,9 @@ func getMetadataUniverseDomain(ctx context.Context) (string, error) {
}
return "", err
}

// FormatIAMServiceAccountName sets a service account name in an IAM resource
// name.
func FormatIAMServiceAccountName(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}

0 comments on commit 260aaaa

Please sign in to comment.