Skip to content

Commit f6dec99

Browse files
authored
feat(idtoken): add support for impersonated_service_account creds type (#1792)
Updates: #873
1 parent ddb5c65 commit f6dec99

File tree

1 file changed

+74
-25
lines changed

1 file changed

+74
-25
lines changed

idtoken/idtoken.go

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import (
99
"encoding/json"
1010
"fmt"
1111
"net/http"
12+
"path/filepath"
13+
"strings"
1214

1315
"cloud.google.com/go/compute/metadata"
1416
"golang.org/x/oauth2"
1517
"golang.org/x/oauth2/google"
1618

19+
"google.golang.org/api/impersonate"
1720
"google.golang.org/api/internal"
1821
"google.golang.org/api/option"
1922
"google.golang.org/api/option/internaloption"
@@ -25,6 +28,14 @@ import (
2528
// ClientOption is for configuring a Google API client or transport.
2629
type ClientOption = option.ClientOption
2730

31+
type credentialsType int
32+
33+
const (
34+
unknownCredType credentialsType = iota
35+
serviceAccount
36+
impersonatedServiceAccount
37+
)
38+
2839
// NewClient creates a HTTP Client that automatically adds an ID token to each
2940
// request via an Authorization header. The token will have the audience
3041
// provided and be configured with the supplied options. The parameter audience
@@ -103,45 +114,83 @@ func newTokenSource(ctx context.Context, audience string, ds *internal.DialSetti
103114
}
104115

105116
func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
106-
if err := isServiceAccount(data); err != nil {
107-
return nil, err
108-
}
109-
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
117+
allowedType, err := getAllowedType(data)
110118
if err != nil {
111119
return nil, err
112120
}
113-
114-
customClaims := ds.CustomClaims
115-
if customClaims == nil {
116-
customClaims = make(map[string]interface{})
121+
switch allowedType {
122+
case serviceAccount:
123+
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
124+
if err != nil {
125+
return nil, err
126+
}
127+
customClaims := ds.CustomClaims
128+
if customClaims == nil {
129+
customClaims = make(map[string]interface{})
130+
}
131+
customClaims["target_audience"] = audience
132+
133+
cfg.PrivateClaims = customClaims
134+
cfg.UseIDToken = true
135+
136+
ts := cfg.TokenSource(ctx)
137+
tok, err := ts.Token()
138+
if err != nil {
139+
return nil, err
140+
}
141+
return oauth2.ReuseTokenSource(tok, ts), nil
142+
case impersonatedServiceAccount:
143+
type url struct {
144+
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
145+
}
146+
var accountURL *url
147+
if err := json.Unmarshal(data, &accountURL); err != nil {
148+
return nil, err
149+
}
150+
account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
151+
account = strings.Split(account, ":")[0]
152+
153+
config := impersonate.IDTokenConfig{
154+
Audience: audience,
155+
TargetPrincipal: account,
156+
IncludeEmail: true,
157+
}
158+
ts, err := impersonate.IDTokenSource(ctx, config)
159+
if err != nil {
160+
return nil, err
161+
}
162+
return ts, nil
163+
default:
164+
return nil, fmt.Errorf("idtoken: unsupported credentials type")
117165
}
118-
customClaims["target_audience"] = audience
119-
120-
cfg.PrivateClaims = customClaims
121-
cfg.UseIDToken = true
122-
123-
ts := cfg.TokenSource(ctx)
124-
tok, err := ts.Token()
125-
if err != nil {
126-
return nil, err
127-
}
128-
return oauth2.ReuseTokenSource(tok, ts), nil
129166
}
130167

131-
func isServiceAccount(data []byte) error {
168+
// getAllowedType returns the credentials type of type credentialsType, and an error.
169+
// allowed types are "service_account" and "impersonated_service_account"
170+
func getAllowedType(data []byte) (credentialsType, error) {
171+
var t credentialsType
132172
if len(data) == 0 {
133-
return fmt.Errorf("idtoken: credential provided is 0 bytes")
173+
return t, fmt.Errorf("idtoken: credential provided is 0 bytes")
134174
}
135175
var f struct {
136176
Type string `json:"type"`
137177
}
138178
if err := json.Unmarshal(data, &f); err != nil {
139-
return err
179+
return t, err
140180
}
141-
if f.Type != "service_account" {
142-
return fmt.Errorf("idtoken: credential must be service_account, found %q", f.Type)
181+
t = parseCredType(f.Type)
182+
return t, nil
183+
}
184+
185+
func parseCredType(typeString string) credentialsType {
186+
switch typeString {
187+
case "service_account":
188+
return serviceAccount
189+
case "impersonated_service_account":
190+
return impersonatedServiceAccount
191+
default:
192+
return unknownCredType
143193
}
144-
return nil
145194
}
146195

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

0 commit comments

Comments
 (0)