4646import datetime
4747import json
4848
49+ import cachetools
50+ from six .moves import urllib
51+
4952from google .auth import _helpers
5053from google .auth import _service_account_info
5154from google .auth import crypt
55+ from google .auth import exceptions
5256import google .auth .credentials
5357
54-
5558_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
59+ _DEFAULT_MAX_CACHE_SIZE = 10
5660
5761
5862def encode (signer , payload , header = None , key_id = None ):
@@ -316,10 +320,10 @@ def __init__(self, signer, issuer, subject, audience,
316320 self ._audience = audience
317321 self ._token_lifetime = token_lifetime
318322
319- if additional_claims is not None :
320- self . _additional_claims = additional_claims
321- else :
322- self ._additional_claims = {}
323+ if additional_claims is None :
324+ additional_claims = {}
325+
326+ self ._additional_claims = additional_claims
323327
324328 @classmethod
325329 def _from_signer_and_info (cls , signer , info , ** kwargs ):
@@ -343,8 +347,7 @@ def _from_signer_and_info(cls, signer, info, **kwargs):
343347
344348 @classmethod
345349 def from_service_account_info (cls , info , ** kwargs ):
346- """Creates a Credentials instance from a dictionary containing service
347- account info in Google format.
350+ """Creates an Credentials instance from a dictionary.
348351
349352 Args:
350353 info (Mapping[str, str]): The service account info in Google
@@ -487,3 +490,266 @@ def signer_email(self):
487490 @_helpers .copy_docstring (google .auth .credentials .Signing )
488491 def signer (self ):
489492 return self ._signer
493+
494+
495+ class OnDemandCredentials (
496+ google .auth .credentials .Signing ,
497+ google .auth .credentials .Credentials ):
498+ """On-demand JWT credentials.
499+
500+ Like :class:`Credentials`, this class uses a JWT as the bearer token for
501+ authentication. However, this class does not require the audience at
502+ construction time. Instead, it will generate a new token on-demand for
503+ each request using the request URI as the audience. It caches tokens
504+ so that multiple requests to the same URI do not incur the overhead
505+ of generating a new token every time.
506+
507+ This behavior is especially useful for `gRPC`_ clients. A gRPC service may
508+ have multiple audience and gRPC clients may not know all of the audiences
509+ required for accessing a particular service. With these credentials,
510+ no knowledge of the audiences is required ahead of time.
511+
512+ .. _grpc: http://www.grpc.io/
513+ """
514+
515+ def __init__ (self , signer , issuer , subject ,
516+ additional_claims = None ,
517+ token_lifetime = _DEFAULT_TOKEN_LIFETIME_SECS ,
518+ max_cache_size = _DEFAULT_MAX_CACHE_SIZE ):
519+ """
520+ Args:
521+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
522+ issuer (str): The `iss` claim.
523+ subject (str): The `sub` claim.
524+ additional_claims (Mapping[str, str]): Any additional claims for
525+ the JWT payload.
526+ token_lifetime (int): The amount of time in seconds for
527+ which the token is valid. Defaults to 1 hour.
528+ max_cache_size (int): The maximum number of JWT tokens to keep in
529+ cache. Tokens are cached using :class:`cachetools.LRUCache`.
530+ """
531+ super (OnDemandCredentials , self ).__init__ ()
532+ self ._signer = signer
533+ self ._issuer = issuer
534+ self ._subject = subject
535+ self ._token_lifetime = token_lifetime
536+
537+ if additional_claims is None :
538+ additional_claims = {}
539+
540+ self ._additional_claims = additional_claims
541+ self ._cache = cachetools .LRUCache (maxsize = max_cache_size )
542+
543+ @classmethod
544+ def _from_signer_and_info (cls , signer , info , ** kwargs ):
545+ """Creates an OnDemandCredentials instance from a signer and service
546+ account info.
547+
548+ Args:
549+ signer (google.auth.crypt.Signer): The signer used to sign JWTs.
550+ info (Mapping[str, str]): The service account info.
551+ kwargs: Additional arguments to pass to the constructor.
552+
553+ Returns:
554+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
555+
556+ Raises:
557+ ValueError: If the info is not in the expected format.
558+ """
559+ kwargs .setdefault ('subject' , info ['client_email' ])
560+ kwargs .setdefault ('issuer' , info ['client_email' ])
561+ return cls (signer , ** kwargs )
562+
563+ @classmethod
564+ def from_service_account_info (cls , info , ** kwargs ):
565+ """Creates an OnDemandCredentials instance from a dictionary.
566+
567+ Args:
568+ info (Mapping[str, str]): The service account info in Google
569+ format.
570+ kwargs: Additional arguments to pass to the constructor.
571+
572+ Returns:
573+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
574+
575+ Raises:
576+ ValueError: If the info is not in the expected format.
577+ """
578+ signer = _service_account_info .from_dict (
579+ info , require = ['client_email' ])
580+ return cls ._from_signer_and_info (signer , info , ** kwargs )
581+
582+ @classmethod
583+ def from_service_account_file (cls , filename , ** kwargs ):
584+ """Creates an OnDemandCredentials instance from a service account .json
585+ file in Google format.
586+
587+ Args:
588+ filename (str): The path to the service account .json file.
589+ kwargs: Additional arguments to pass to the constructor.
590+
591+ Returns:
592+ google.auth.jwt.OnDemandCredentials: The constructed credentials.
593+ """
594+ info , signer = _service_account_info .from_filename (
595+ filename , require = ['client_email' ])
596+ return cls ._from_signer_and_info (signer , info , ** kwargs )
597+
598+ @classmethod
599+ def from_signing_credentials (cls , credentials , ** kwargs ):
600+ """Creates a new :class:`google.auth.jwt.OnDemandCredentials` instance
601+ from an existing :class:`google.auth.credentials.Signing` instance.
602+
603+ The new instance will use the same signer as the existing instance and
604+ will use the existing instance's signer email as the issuer and
605+ subject by default.
606+
607+ Example::
608+
609+ svc_creds = service_account.Credentials.from_service_account_file(
610+ 'service_account.json')
611+ jwt_creds = jwt.OnDemandCredentials.from_signing_credentials(
612+ svc_creds)
613+
614+ Args:
615+ credentials (google.auth.credentials.Signing): The credentials to
616+ use to construct the new credentials.
617+ kwargs: Additional arguments to pass to the constructor.
618+
619+ Returns:
620+ google.auth.jwt.Credentials: A new Credentials instance.
621+ """
622+ kwargs .setdefault ('issuer' , credentials .signer_email )
623+ kwargs .setdefault ('subject' , credentials .signer_email )
624+ return cls (credentials .signer , ** kwargs )
625+
626+ def with_claims (self , issuer = None , subject = None , additional_claims = None ):
627+ """Returns a copy of these credentials with modified claims.
628+
629+ Args:
630+ issuer (str): The `iss` claim. If unspecified the current issuer
631+ claim will be used.
632+ subject (str): The `sub` claim. If unspecified the current subject
633+ claim will be used.
634+ additional_claims (Mapping[str, str]): Any additional claims for
635+ the JWT payload. This will be merged with the current
636+ additional claims.
637+
638+ Returns:
639+ google.auth.jwt.OnDemandCredentials: A new credentials instance.
640+ """
641+ new_additional_claims = copy .deepcopy (self ._additional_claims )
642+ new_additional_claims .update (additional_claims or {})
643+
644+ return OnDemandCredentials (
645+ self ._signer ,
646+ issuer = issuer if issuer is not None else self ._issuer ,
647+ subject = subject if subject is not None else self ._subject ,
648+ additional_claims = new_additional_claims ,
649+ max_cache_size = self ._cache .maxsize )
650+
651+ @property
652+ def valid (self ):
653+ """Checks the validity of the credentials.
654+
655+ These credentials are always valid because it generates tokens on
656+ demand.
657+ """
658+ return True
659+
660+ def _make_jwt_for_audience (self , audience ):
661+ """Make a new JWT for the given audience.
662+
663+ Args:
664+ audience (str): The intended audience.
665+
666+ Returns:
667+ Tuple[bytes, datetime]: The encoded JWT and the expiration.
668+ """
669+ now = _helpers .utcnow ()
670+ lifetime = datetime .timedelta (seconds = self ._token_lifetime )
671+ expiry = now + lifetime
672+
673+ payload = {
674+ 'iss' : self ._issuer ,
675+ 'sub' : self ._subject ,
676+ 'iat' : _helpers .datetime_to_secs (now ),
677+ 'exp' : _helpers .datetime_to_secs (expiry ),
678+ 'aud' : audience ,
679+ }
680+
681+ payload .update (self ._additional_claims )
682+
683+ jwt = encode (self ._signer , payload )
684+
685+ return jwt , expiry
686+
687+ def _get_jwt_for_audience (self , audience ):
688+ """Get a JWT For a given audience.
689+
690+ If there is already an existing, non-expired token in the cache for
691+ the audience, that token is used. Otherwise, a new token will be
692+ created.
693+
694+ Args:
695+ audience (str): The intended audience.
696+
697+ Returns:
698+ bytes: The encoded JWT.
699+ """
700+ token , expiry = self ._cache .get (audience , (None , None ))
701+
702+ if token is None or expiry < _helpers .utcnow ():
703+ token , expiry = self ._make_jwt_for_audience (audience )
704+ self ._cache [audience ] = token , expiry
705+
706+ return token
707+
708+ def refresh (self , request ):
709+ """Raises an exception, these credentials can not be directly
710+ refreshed.
711+
712+ Args:
713+ request (Any): Unused.
714+
715+ Raises:
716+ google.auth.RefreshError
717+ """
718+ # pylint: disable=unused-argument
719+ # (pylint doesn't correctly recognize overridden methods.)
720+ raise exceptions .RefreshError (
721+ 'OnDemandCredentials can not be directly refreshed.' )
722+
723+ def before_request (self , request , method , url , headers ):
724+ """Performs credential-specific before request logic.
725+
726+ Args:
727+ request (Any): Unused. JWT credentials do not need to make an
728+ HTTP request to refresh.
729+ method (str): The request's HTTP method.
730+ url (str): The request's URI. This is used as the audience claim
731+ when generating the JWT.
732+ headers (Mapping): The request's headers.
733+ """
734+ # pylint: disable=unused-argument
735+ # (pylint doesn't correctly recognize overridden methods.)
736+ parts = urllib .parse .urlsplit (url )
737+ # Strip query string and fragment
738+ audience = urllib .parse .urlunsplit (
739+ (parts .scheme , parts .netloc , parts .path , None , None ))
740+ token = self ._get_jwt_for_audience (audience )
741+ self .apply (headers , token = token )
742+
743+ @_helpers .copy_docstring (google .auth .credentials .Signing )
744+ def sign_bytes (self , message ):
745+ return self ._signer .sign (message )
746+
747+ @property
748+ @_helpers .copy_docstring (google .auth .credentials .Signing )
749+ def signer_email (self ):
750+ return self ._issuer
751+
752+ @property
753+ @_helpers .copy_docstring (google .auth .credentials .Signing )
754+ def signer (self ):
755+ return self ._signer
0 commit comments