3838from google .auth import iam
3939from google .auth import jwt
4040from google .auth import metrics
41+ from google .oauth2 import _client
4142
4243
4344_REFRESH_ERROR = "Unable to acquire impersonated credentials"
4445
4546_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
4647
48+ _GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
49+
4750
4851def _make_iam_token_request (
4952 request ,
@@ -177,6 +180,7 @@ def __init__(
177180 target_principal ,
178181 target_scopes ,
179182 delegates = None ,
183+ subject = None ,
180184 lifetime = _DEFAULT_TOKEN_LIFETIME_SECS ,
181185 quota_project_id = None ,
182186 iam_endpoint_override = None ,
@@ -204,9 +208,12 @@ def __init__(
204208 quota_project_id (Optional[str]): The project ID used for quota and billing.
205209 This project may be different from the project used to
206210 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
208212 with the target_principal embedded. This is useful when supporting
209213 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.
210217 """
211218
212219 super (Credentials , self ).__init__ ()
@@ -231,6 +238,7 @@ def __init__(
231238 self ._target_principal = target_principal
232239 self ._target_scopes = target_scopes
233240 self ._delegates = delegates
241+ self ._subject = subject
234242 self ._lifetime = lifetime or _DEFAULT_TOKEN_LIFETIME_SECS
235243 self .token = None
236244 self .expiry = _helpers .utcnow ()
@@ -275,6 +283,39 @@ def _update_token(self, request):
275283 # Apply the source credentials authentication info.
276284 self ._source_credentials .apply (headers )
277285
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+
278319 self .token , self .expiry = _make_iam_token_request (
279320 request = request ,
280321 principal = self ._target_principal ,
@@ -478,3 +519,61 @@ def refresh(self, request):
478519 self .expiry = datetime .utcfromtimestamp (
479520 jwt .decode (id_token , verify = False )["exp" ]
480521 )
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