1
2
3
4
5 package google
6
7 import (
8 "context"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "net/url"
13 "strings"
14 "time"
15
16 "cloud.google.com/go/compute/metadata"
17 "golang.org/x/oauth2"
18 "golang.org/x/oauth2/google/externalaccount"
19 "golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
20 "golang.org/x/oauth2/google/internal/impersonate"
21 "golang.org/x/oauth2/jwt"
22 )
23
24
25 var Endpoint = oauth2.Endpoint{
26 AuthURL: "https://accounts.google.com/o/oauth2/auth",
27 TokenURL: "https://oauth2.googleapis.com/token",
28 DeviceAuthURL: "https://oauth2.googleapis.com/device/code",
29 AuthStyle: oauth2.AuthStyleInParams,
30 }
31
32
33 const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
34
35
36 const JWTTokenURL = "https://oauth2.googleapis.com/token"
37
38
39
40
41
42
43
44 func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
45 type cred struct {
46 ClientID string `json:"client_id"`
47 ClientSecret string `json:"client_secret"`
48 RedirectURIs []string `json:"redirect_uris"`
49 AuthURI string `json:"auth_uri"`
50 TokenURI string `json:"token_uri"`
51 }
52 var j struct {
53 Web *cred `json:"web"`
54 Installed *cred `json:"installed"`
55 }
56 if err := json.Unmarshal(jsonKey, &j); err != nil {
57 return nil, err
58 }
59 var c *cred
60 switch {
61 case j.Web != nil:
62 c = j.Web
63 case j.Installed != nil:
64 c = j.Installed
65 default:
66 return nil, fmt.Errorf("oauth2/google: no credentials found")
67 }
68 if len(c.RedirectURIs) < 1 {
69 return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
70 }
71 return &oauth2.Config{
72 ClientID: c.ClientID,
73 ClientSecret: c.ClientSecret,
74 RedirectURL: c.RedirectURIs[0],
75 Scopes: scope,
76 Endpoint: oauth2.Endpoint{
77 AuthURL: c.AuthURI,
78 TokenURL: c.TokenURI,
79 },
80 }, nil
81 }
82
83
84
85
86
87 func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
88 var f credentialsFile
89 if err := json.Unmarshal(jsonKey, &f); err != nil {
90 return nil, err
91 }
92 if f.Type != serviceAccountKey {
93 return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
94 }
95 scope = append([]string(nil), scope...)
96 return f.jwtConfig(scope, ""), nil
97 }
98
99
100 const (
101 serviceAccountKey = "service_account"
102 userCredentialsKey = "authorized_user"
103 externalAccountKey = "external_account"
104 externalAccountAuthorizedUserKey = "external_account_authorized_user"
105 impersonatedServiceAccount = "impersonated_service_account"
106 )
107
108
109 type credentialsFile struct {
110 Type string `json:"type"`
111
112
113 ClientEmail string `json:"client_email"`
114 PrivateKeyID string `json:"private_key_id"`
115 PrivateKey string `json:"private_key"`
116 AuthURL string `json:"auth_uri"`
117 TokenURL string `json:"token_uri"`
118 ProjectID string `json:"project_id"`
119 UniverseDomain string `json:"universe_domain"`
120
121
122
123 ClientSecret string `json:"client_secret"`
124 ClientID string `json:"client_id"`
125 RefreshToken string `json:"refresh_token"`
126
127
128 Audience string `json:"audience"`
129 SubjectTokenType string `json:"subject_token_type"`
130 TokenURLExternal string `json:"token_url"`
131 TokenInfoURL string `json:"token_info_url"`
132 ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
133 ServiceAccountImpersonation serviceAccountImpersonationInfo `json:"service_account_impersonation"`
134 Delegates []string `json:"delegates"`
135 CredentialSource externalaccount.CredentialSource `json:"credential_source"`
136 QuotaProjectID string `json:"quota_project_id"`
137 WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
138
139
140 RevokeURL string `json:"revoke_url"`
141
142
143 SourceCredentials *credentialsFile `json:"source_credentials"`
144 }
145
146 type serviceAccountImpersonationInfo struct {
147 TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
148 }
149
150 func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
151 cfg := &jwt.Config{
152 Email: f.ClientEmail,
153 PrivateKey: []byte(f.PrivateKey),
154 PrivateKeyID: f.PrivateKeyID,
155 Scopes: scopes,
156 TokenURL: f.TokenURL,
157 Subject: subject,
158 Audience: f.Audience,
159 }
160 if cfg.TokenURL == "" {
161 cfg.TokenURL = JWTTokenURL
162 }
163 return cfg
164 }
165
166 func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
167 switch f.Type {
168 case serviceAccountKey:
169 cfg := f.jwtConfig(params.Scopes, params.Subject)
170 return cfg.TokenSource(ctx), nil
171 case userCredentialsKey:
172 cfg := &oauth2.Config{
173 ClientID: f.ClientID,
174 ClientSecret: f.ClientSecret,
175 Scopes: params.Scopes,
176 Endpoint: oauth2.Endpoint{
177 AuthURL: f.AuthURL,
178 TokenURL: f.TokenURL,
179 AuthStyle: oauth2.AuthStyleInParams,
180 },
181 }
182 if cfg.Endpoint.AuthURL == "" {
183 cfg.Endpoint.AuthURL = Endpoint.AuthURL
184 }
185 if cfg.Endpoint.TokenURL == "" {
186 if params.TokenURL != "" {
187 cfg.Endpoint.TokenURL = params.TokenURL
188 } else {
189 cfg.Endpoint.TokenURL = Endpoint.TokenURL
190 }
191 }
192 tok := &oauth2.Token{RefreshToken: f.RefreshToken}
193 return cfg.TokenSource(ctx, tok), nil
194 case externalAccountKey:
195 cfg := &externalaccount.Config{
196 Audience: f.Audience,
197 SubjectTokenType: f.SubjectTokenType,
198 TokenURL: f.TokenURLExternal,
199 TokenInfoURL: f.TokenInfoURL,
200 ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
201 ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
202 ClientSecret: f.ClientSecret,
203 ClientID: f.ClientID,
204 CredentialSource: &f.CredentialSource,
205 QuotaProjectID: f.QuotaProjectID,
206 Scopes: params.Scopes,
207 WorkforcePoolUserProject: f.WorkforcePoolUserProject,
208 }
209 return externalaccount.NewTokenSource(ctx, *cfg)
210 case externalAccountAuthorizedUserKey:
211 cfg := &externalaccountauthorizeduser.Config{
212 Audience: f.Audience,
213 RefreshToken: f.RefreshToken,
214 TokenURL: f.TokenURLExternal,
215 TokenInfoURL: f.TokenInfoURL,
216 ClientID: f.ClientID,
217 ClientSecret: f.ClientSecret,
218 RevokeURL: f.RevokeURL,
219 QuotaProjectID: f.QuotaProjectID,
220 Scopes: params.Scopes,
221 }
222 return cfg.TokenSource(ctx)
223 case impersonatedServiceAccount:
224 if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
225 return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
226 }
227
228 ts, err := f.SourceCredentials.tokenSource(ctx, params)
229 if err != nil {
230 return nil, err
231 }
232 imp := impersonate.ImpersonateTokenSource{
233 Ctx: ctx,
234 URL: f.ServiceAccountImpersonationURL,
235 Scopes: params.Scopes,
236 Ts: ts,
237 Delegates: f.Delegates,
238 }
239 return oauth2.ReuseTokenSource(nil, imp), nil
240 case "":
241 return nil, errors.New("missing 'type' field in credentials")
242 default:
243 return nil, fmt.Errorf("unknown credential type: %q", f.Type)
244 }
245 }
246
247
248
249
250
251
252
253
254 func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
255 return computeTokenSource(account, 0, scope...)
256 }
257
258 func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
259 return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
260 }
261
262 type computeSource struct {
263 account string
264 scopes []string
265 }
266
267 func (cs computeSource) Token() (*oauth2.Token, error) {
268 if !metadata.OnGCE() {
269 return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
270 }
271 acct := cs.account
272 if acct == "" {
273 acct = "default"
274 }
275 tokenURI := "instance/service-accounts/" + acct + "/token"
276 if len(cs.scopes) > 0 {
277 v := url.Values{}
278 v.Set("scopes", strings.Join(cs.scopes, ","))
279 tokenURI = tokenURI + "?" + v.Encode()
280 }
281 tokenJSON, err := metadata.Get(tokenURI)
282 if err != nil {
283 return nil, err
284 }
285 var res struct {
286 AccessToken string `json:"access_token"`
287 ExpiresInSec int `json:"expires_in"`
288 TokenType string `json:"token_type"`
289 }
290 err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
291 if err != nil {
292 return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
293 }
294 if res.ExpiresInSec == 0 || res.AccessToken == "" {
295 return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
296 }
297 tok := &oauth2.Token{
298 AccessToken: res.AccessToken,
299 TokenType: res.TokenType,
300 Expiry: time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
301 }
302
303
304
305 return tok.WithExtra(map[string]interface{}{
306 "oauth2.google.tokenSource": "compute-metadata",
307 "oauth2.google.serviceAccount": acct,
308 }), nil
309 }
310
View as plain text