1
2
3
4
5 package idtoken
6
7 import (
8 "context"
9 "encoding/json"
10 "fmt"
11 "net/http"
12 "path/filepath"
13 "strings"
14
15 "cloud.google.com/go/compute/metadata"
16 "golang.org/x/oauth2"
17 "golang.org/x/oauth2/google"
18
19 newidtoken "cloud.google.com/go/auth/credentials/idtoken"
20 "cloud.google.com/go/auth/oauth2adapt"
21 "google.golang.org/api/impersonate"
22 "google.golang.org/api/internal"
23 "google.golang.org/api/option"
24 "google.golang.org/api/option/internaloption"
25 htransport "google.golang.org/api/transport/http"
26 )
27
28
29
30
31 type ClientOption = option.ClientOption
32
33 type credentialsType int
34
35 const (
36 unknownCredType credentialsType = iota
37 serviceAccount
38 impersonatedServiceAccount
39 externalAccount
40 )
41
42
43
44
45
46 func NewClient(ctx context.Context, audience string, opts ...ClientOption) (*http.Client, error) {
47 var ds internal.DialSettings
48 for _, opt := range opts {
49 opt.Apply(&ds)
50 }
51 if err := ds.Validate(); err != nil {
52 return nil, err
53 }
54 if ds.NoAuth {
55 return nil, fmt.Errorf("idtoken: option.WithoutAuthentication not supported")
56 }
57 if ds.APIKey != "" {
58 return nil, fmt.Errorf("idtoken: option.WithAPIKey not supported")
59 }
60 if ds.TokenSource != nil {
61 return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported")
62 }
63
64 ts, err := NewTokenSource(ctx, audience, opts...)
65 if err != nil {
66 return nil, err
67 }
68
69
70 opts = append(opts, option.WithTokenSource(ts), internaloption.SkipDialSettingsValidation())
71 httpTransport := http.DefaultTransport.(*http.Transport).Clone()
72 httpTransport.MaxIdleConnsPerHost = 100
73 t, err := htransport.NewTransport(ctx, httpTransport, opts...)
74 if err != nil {
75 return nil, err
76 }
77 return &http.Client{Transport: t}, nil
78 }
79
80
81
82
83 func NewTokenSource(ctx context.Context, audience string, opts ...ClientOption) (oauth2.TokenSource, error) {
84 if audience == "" {
85 return nil, fmt.Errorf("idtoken: must supply a non-empty audience")
86 }
87 var ds internal.DialSettings
88 for _, opt := range opts {
89 opt.Apply(&ds)
90 }
91 if err := ds.Validate(); err != nil {
92 return nil, err
93 }
94 if ds.TokenSource != nil {
95 return nil, fmt.Errorf("idtoken: option.WithTokenSource not supported")
96 }
97 if ds.ImpersonationConfig != nil {
98 return nil, fmt.Errorf("idtoken: option.WithImpersonatedCredentials not supported")
99 }
100 if ds.IsNewAuthLibraryEnabled() {
101 return newTokenSourceNewAuth(ctx, audience, &ds)
102 }
103 return newTokenSource(ctx, audience, &ds)
104 }
105
106 func newTokenSourceNewAuth(ctx context.Context, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
107 if ds.AuthCredentials != nil {
108 return nil, fmt.Errorf("idtoken: option.WithTokenProvider not supported")
109 }
110 creds, err := newidtoken.NewCredentials(&newidtoken.Options{
111 Audience: audience,
112 CustomClaims: ds.CustomClaims,
113 CredentialsFile: ds.CredentialsFile,
114 CredentialsJSON: ds.CredentialsJSON,
115 Client: oauth2.NewClient(ctx, nil),
116 })
117 if err != nil {
118 return nil, err
119 }
120 return oauth2adapt.TokenSourceFromTokenProvider(creds), nil
121 }
122
123 func newTokenSource(ctx context.Context, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
124 creds, err := internal.Creds(ctx, ds)
125 if err != nil {
126 return nil, err
127 }
128 if len(creds.JSON) > 0 {
129 return tokenSourceFromBytes(ctx, creds.JSON, audience, ds)
130 }
131
132
133 if metadata.OnGCE() {
134 return computeTokenSource(audience, ds)
135 }
136 return nil, fmt.Errorf("idtoken: couldn't find any credentials")
137 }
138
139 func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) {
140 allowedType, err := getAllowedType(data)
141 if err != nil {
142 return nil, err
143 }
144 switch allowedType {
145 case serviceAccount:
146 cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
147 if err != nil {
148 return nil, err
149 }
150 customClaims := ds.CustomClaims
151 if customClaims == nil {
152 customClaims = make(map[string]interface{})
153 }
154 customClaims["target_audience"] = audience
155
156 cfg.PrivateClaims = customClaims
157 cfg.UseIDToken = true
158
159 ts := cfg.TokenSource(ctx)
160 tok, err := ts.Token()
161 if err != nil {
162 return nil, err
163 }
164 return oauth2.ReuseTokenSource(tok, ts), nil
165 case impersonatedServiceAccount, externalAccount:
166 type url struct {
167 ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
168 }
169 var accountURL *url
170 if err := json.Unmarshal(data, &accountURL); err != nil {
171 return nil, err
172 }
173 account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
174 account = strings.Split(account, ":")[0]
175
176 config := impersonate.IDTokenConfig{
177 Audience: audience,
178 TargetPrincipal: account,
179 IncludeEmail: true,
180 }
181 ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))
182 if err != nil {
183 return nil, err
184 }
185 return ts, nil
186 default:
187 return nil, fmt.Errorf("idtoken: unsupported credentials type")
188 }
189 }
190
191
192
193 func getAllowedType(data []byte) (credentialsType, error) {
194 var t credentialsType
195 if len(data) == 0 {
196 return t, fmt.Errorf("idtoken: credential provided is 0 bytes")
197 }
198 var f struct {
199 Type string `json:"type"`
200 }
201 if err := json.Unmarshal(data, &f); err != nil {
202 return t, err
203 }
204 t = parseCredType(f.Type)
205 return t, nil
206 }
207
208 func parseCredType(typeString string) credentialsType {
209 switch typeString {
210 case "service_account":
211 return serviceAccount
212 case "impersonated_service_account":
213 return impersonatedServiceAccount
214 case "external_account":
215 return externalAccount
216 default:
217 return unknownCredType
218 }
219 }
220
221
222 func WithCustomClaims(customClaims map[string]interface{}) ClientOption {
223 return withCustomClaims(customClaims)
224 }
225
226 type withCustomClaims map[string]interface{}
227
228 func (w withCustomClaims) Apply(o *internal.DialSettings) {
229 o.CustomClaims = w
230 }
231
232
233
234
235 func WithCredentialsFile(filename string) ClientOption {
236 return option.WithCredentialsFile(filename)
237 }
238
239
240
241
242 func WithCredentialsJSON(p []byte) ClientOption {
243 return option.WithCredentialsJSON(p)
244 }
245
246
247
248
249
250 func WithHTTPClient(client *http.Client) ClientOption {
251 return option.WithHTTPClient(client)
252 }
253
View as plain text