38
38
from google .auth import iam
39
39
from google .auth import jwt
40
40
from google .auth import metrics
41
+ from google .oauth2 import _client
41
42
42
43
43
44
_REFRESH_ERROR = "Unable to acquire impersonated credentials"
44
45
45
46
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
46
47
48
+ _GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
49
+
47
50
48
51
def _make_iam_token_request (
49
52
request ,
@@ -177,6 +180,7 @@ def __init__(
177
180
target_principal ,
178
181
target_scopes ,
179
182
delegates = None ,
183
+ subject = None ,
180
184
lifetime = _DEFAULT_TOKEN_LIFETIME_SECS ,
181
185
quota_project_id = None ,
182
186
iam_endpoint_override = None ,
@@ -204,9 +208,12 @@ def __init__(
204
208
quota_project_id (Optional[str]): The project ID used for quota and billing.
205
209
This project may be different from the project used to
206
210
create the credentials.
207
- iam_endpoint_override (Optiona [str]): The full IAM endpoint override
211
+ iam_endpoint_override (Optional [str]): The full IAM endpoint override
208
212
with the target_principal embedded. This is useful when supporting
209
213
impersonation with regional endpoints.
214
+ subject (Optional[str]): sub field of a JWT. This field should only be set
215
+ if you wish to impersonate as a user. This feature is useful when
216
+ using domain wide delegation.
210
217
"""
211
218
212
219
super (Credentials , self ).__init__ ()
@@ -231,6 +238,7 @@ def __init__(
231
238
self ._target_principal = target_principal
232
239
self ._target_scopes = target_scopes
233
240
self ._delegates = delegates
241
+ self ._subject = subject
234
242
self ._lifetime = lifetime or _DEFAULT_TOKEN_LIFETIME_SECS
235
243
self .token = None
236
244
self .expiry = _helpers .utcnow ()
@@ -275,6 +283,39 @@ def _update_token(self, request):
275
283
# Apply the source credentials authentication info.
276
284
self ._source_credentials .apply (headers )
277
285
286
+ # If a subject is specified a domain-wide delegation auth-flow is initiated
287
+ # to impersonate as the provided subject (user).
288
+ if self ._subject :
289
+ if self .universe_domain != credentials .DEFAULT_UNIVERSE_DOMAIN :
290
+ raise exceptions .GoogleAuthError (
291
+ "Domain-wide delegation is not supported in universes other "
292
+ + "than googleapis.com"
293
+ )
294
+
295
+ now = _helpers .utcnow ()
296
+ payload = {
297
+ "iss" : self ._target_principal ,
298
+ "scope" : _helpers .scopes_to_string (self ._target_scopes or ()),
299
+ "sub" : self ._subject ,
300
+ "aud" : _GOOGLE_OAUTH2_TOKEN_ENDPOINT ,
301
+ "iat" : _helpers .datetime_to_secs (now ),
302
+ "exp" : _helpers .datetime_to_secs (now ) + _DEFAULT_TOKEN_LIFETIME_SECS ,
303
+ }
304
+
305
+ assertion = _sign_jwt_request (
306
+ request = request ,
307
+ principal = self ._target_principal ,
308
+ headers = headers ,
309
+ payload = payload ,
310
+ delegates = self ._delegates ,
311
+ )
312
+
313
+ self .token , self .expiry , _ = _client .jwt_grant (
314
+ request , _GOOGLE_OAUTH2_TOKEN_ENDPOINT , assertion
315
+ )
316
+
317
+ return
318
+
278
319
self .token , self .expiry = _make_iam_token_request (
279
320
request = request ,
280
321
principal = self ._target_principal ,
@@ -478,3 +519,61 @@ def refresh(self, request):
478
519
self .expiry = datetime .utcfromtimestamp (
479
520
jwt .decode (id_token , verify = False )["exp" ]
480
521
)
522
+
523
+
524
+ def _sign_jwt_request (request , principal , headers , payload , delegates = []):
525
+ """Makes a request to the Google Cloud IAM service to sign a JWT using a
526
+ service account's system-managed private key.
527
+ Args:
528
+ request (Request): The Request object to use.
529
+ principal (str): The principal to request an access token for.
530
+ headers (Mapping[str, str]): Map of headers to transmit.
531
+ payload (Mapping[str, str]): The JWT payload to sign. Must be a
532
+ serialized JSON object that contains a JWT Claims Set.
533
+ delegates (Sequence[str]): The chained list of delegates required
534
+ to grant the final access_token. If set, the sequence of
535
+ identities must have "Service Account Token Creator" capability
536
+ granted to the prceeding identity. For example, if set to
537
+ [serviceAccountB, serviceAccountC], the source_credential
538
+ must have the Token Creator role on serviceAccountB.
539
+ serviceAccountB must have the Token Creator on
540
+ serviceAccountC.
541
+ Finally, C must have Token Creator on target_principal.
542
+ If left unset, source_credential must have that role on
543
+ target_principal.
544
+
545
+ Raises:
546
+ google.auth.exceptions.TransportError: Raised if there is an underlying
547
+ HTTP connection error
548
+ google.auth.exceptions.RefreshError: Raised if the impersonated
549
+ credentials are not available. Common reasons are
550
+ `iamcredentials.googleapis.com` is not enabled or the
551
+ `Service Account Token Creator` is not assigned
552
+ """
553
+ iam_endpoint = iam ._IAM_SIGNJWT_ENDPOINT .format (principal )
554
+
555
+ body = {"delegates" : delegates , "payload" : json .dumps (payload )}
556
+ body = json .dumps (body ).encode ("utf-8" )
557
+
558
+ response = request (url = iam_endpoint , method = "POST" , headers = headers , body = body )
559
+
560
+ # support both string and bytes type response.data
561
+ response_body = (
562
+ response .data .decode ("utf-8" )
563
+ if hasattr (response .data , "decode" )
564
+ else response .data
565
+ )
566
+
567
+ if response .status != http_client .OK :
568
+ raise exceptions .RefreshError (_REFRESH_ERROR , response_body )
569
+
570
+ try :
571
+ jwt_response = json .loads (response_body )
572
+ signed_jwt = jwt_response ["signedJwt" ]
573
+ return signed_jwt
574
+
575
+ except (KeyError , ValueError ) as caught_exc :
576
+ new_exc = exceptions .RefreshError (
577
+ "{}: No signed JWT in response." .format (_REFRESH_ERROR ), response_body
578
+ )
579
+ raise new_exc from caught_exc
0 commit comments