1# Copyright 2016 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0
16
17This module implements the JWT Profile for OAuth 2.0 Authorization Grants
18as defined by `RFC 7523`_ with particular support for how this RFC is
19implemented in Google's infrastructure. Google refers to these credentials
20as *Service Accounts*.
21
22Service accounts are used for server-to-server communication, such as
23interactions between a web application server and a Google service. The
24service account belongs to your application instead of to an individual end
25user. In contrast to other OAuth 2.0 profiles, no users are involved and your
26application "acts" as the service account.
27
28Typically an application uses a service account when the application uses
29Google APIs to work with its own data rather than a user's data. For example,
30an application that uses Google Cloud Datastore for data persistence would use
31a service account to authenticate its calls to the Google Cloud Datastore API.
32However, an application that needs to access a user's Drive documents would
33use the normal OAuth 2.0 profile.
34
35Additionally, Google Apps domain administrators can grant service accounts
36`domain-wide delegation`_ authority to access user data on behalf of users in
37the domain.
38
39This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used
40in place of the usual authorization token returned during the standard
41OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as
42the acquired access token is used as the bearer token when making requests
43using these credentials.
44
45This profile differs from normal OAuth 2.0 profile because no user consent
46step is required. The use of the private key allows this profile to assert
47identity directly.
48
49This profile also differs from the :mod:`google.auth.jwt` authentication
50because the JWT credentials use the JWT directly as the bearer token. This
51profile instead only uses the JWT to obtain an OAuth 2.0 access token. The
52obtained OAuth 2.0 access token is used as the bearer token.
53
54Domain-wide delegation
55----------------------
56
57Domain-wide delegation allows a service account to access user data on
58behalf of any user in a Google Apps domain without consent from the user.
59For example, an application that uses the Google Calendar API to add events to
60the calendars of all users in a Google Apps domain would use a service account
61to access the Google Calendar API on behalf of users.
62
63The Google Apps administrator must explicitly authorize the service account to
64do this. This authorization step is referred to as "delegating domain-wide
65authority" to a service account.
66
67You can use domain-wise delegation by creating a set of credentials with a
68specific subject using :meth:`~Credentials.with_subject`.
69
70.. _RFC 7523: https://tools.ietf.org/html/rfc7523
71"""
72
73import copy
74import datetime
75
76from google.auth import _helpers
77from google.auth import _service_account_info
78from google.auth import credentials
79from google.auth import jwt
80from google.oauth2 import _client
81
82_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
83_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
84
85
86class Credentials(
87    credentials.Signing, credentials.Scoped, credentials.CredentialsWithQuotaProject
88):
89    """Service account credentials
90
91    Usually, you'll create these credentials with one of the helper
92    constructors. To create credentials using a Google service account
93    private key JSON file::
94
95        credentials = service_account.Credentials.from_service_account_file(
96            'service-account.json')
97
98    Or if you already have the service account file loaded::
99
100        service_account_info = json.load(open('service_account.json'))
101        credentials = service_account.Credentials.from_service_account_info(
102            service_account_info)
103
104    Both helper methods pass on arguments to the constructor, so you can
105    specify additional scopes and a subject if necessary::
106
107        credentials = service_account.Credentials.from_service_account_file(
108            'service-account.json',
109            scopes=['email'],
110            subject='[email protected]')
111
112    The credentials are considered immutable. If you want to modify the scopes
113    or the subject used for delegation, use :meth:`with_scopes` or
114    :meth:`with_subject`::
115
116        scoped_credentials = credentials.with_scopes(['email'])
117        delegated_credentials = credentials.with_subject(subject)
118
119    To add a quota project, use :meth:`with_quota_project`::
120
121        credentials = credentials.with_quota_project('myproject-123')
122    """
123
124    def __init__(
125        self,
126        signer,
127        service_account_email,
128        token_uri,
129        scopes=None,
130        default_scopes=None,
131        subject=None,
132        project_id=None,
133        quota_project_id=None,
134        additional_claims=None,
135        always_use_jwt_access=False,
136    ):
137        """
138        Args:
139            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
140            service_account_email (str): The service account's email.
141            scopes (Sequence[str]): User-defined scopes to request during the
142                authorization grant.
143            default_scopes (Sequence[str]): Default scopes passed by a
144                Google client library. Use 'scopes' for user-defined scopes.
145            token_uri (str): The OAuth 2.0 Token URI.
146            subject (str): For domain-wide delegation, the email address of the
147                user to for which to request delegated access.
148            project_id  (str): Project ID associated with the service account
149                credential.
150            quota_project_id (Optional[str]): The project ID used for quota and
151                billing.
152            additional_claims (Mapping[str, str]): Any additional claims for
153                the JWT assertion used in the authorization grant.
154            always_use_jwt_access (Optional[bool]): Whether self signed JWT should
155                be always used.
156
157        .. note:: Typically one of the helper constructors
158            :meth:`from_service_account_file` or
159            :meth:`from_service_account_info` are used instead of calling the
160            constructor directly.
161        """
162        super(Credentials, self).__init__()
163
164        self._scopes = scopes
165        self._default_scopes = default_scopes
166        self._signer = signer
167        self._service_account_email = service_account_email
168        self._subject = subject
169        self._project_id = project_id
170        self._quota_project_id = quota_project_id
171        self._token_uri = token_uri
172        self._always_use_jwt_access = always_use_jwt_access
173
174        self._jwt_credentials = None
175
176        if additional_claims is not None:
177            self._additional_claims = additional_claims
178        else:
179            self._additional_claims = {}
180
181    @classmethod
182    def _from_signer_and_info(cls, signer, info, **kwargs):
183        """Creates a Credentials instance from a signer and service account
184        info.
185
186        Args:
187            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
188            info (Mapping[str, str]): The service account info.
189            kwargs: Additional arguments to pass to the constructor.
190
191        Returns:
192            google.auth.jwt.Credentials: The constructed credentials.
193
194        Raises:
195            ValueError: If the info is not in the expected format.
196        """
197        return cls(
198            signer,
199            service_account_email=info["client_email"],
200            token_uri=info["token_uri"],
201            project_id=info.get("project_id"),
202            **kwargs
203        )
204
205    @classmethod
206    def from_service_account_info(cls, info, **kwargs):
207        """Creates a Credentials instance from parsed service account info.
208
209        Args:
210            info (Mapping[str, str]): The service account info in Google
211                format.
212            kwargs: Additional arguments to pass to the constructor.
213
214        Returns:
215            google.auth.service_account.Credentials: The constructed
216                credentials.
217
218        Raises:
219            ValueError: If the info is not in the expected format.
220        """
221        signer = _service_account_info.from_dict(
222            info, require=["client_email", "token_uri"]
223        )
224        return cls._from_signer_and_info(signer, info, **kwargs)
225
226    @classmethod
227    def from_service_account_file(cls, filename, **kwargs):
228        """Creates a Credentials instance from a service account json file.
229
230        Args:
231            filename (str): The path to the service account json file.
232            kwargs: Additional arguments to pass to the constructor.
233
234        Returns:
235            google.auth.service_account.Credentials: The constructed
236                credentials.
237        """
238        info, signer = _service_account_info.from_filename(
239            filename, require=["client_email", "token_uri"]
240        )
241        return cls._from_signer_and_info(signer, info, **kwargs)
242
243    @property
244    def service_account_email(self):
245        """The service account email."""
246        return self._service_account_email
247
248    @property
249    def project_id(self):
250        """Project ID associated with this credential."""
251        return self._project_id
252
253    @property
254    def requires_scopes(self):
255        """Checks if the credentials requires scopes.
256
257        Returns:
258            bool: True if there are no scopes set otherwise False.
259        """
260        return True if not self._scopes else False
261
262    @_helpers.copy_docstring(credentials.Scoped)
263    def with_scopes(self, scopes, default_scopes=None):
264        return self.__class__(
265            self._signer,
266            service_account_email=self._service_account_email,
267            scopes=scopes,
268            default_scopes=default_scopes,
269            token_uri=self._token_uri,
270            subject=self._subject,
271            project_id=self._project_id,
272            quota_project_id=self._quota_project_id,
273            additional_claims=self._additional_claims.copy(),
274            always_use_jwt_access=self._always_use_jwt_access,
275        )
276
277    def with_always_use_jwt_access(self, always_use_jwt_access):
278        """Create a copy of these credentials with the specified always_use_jwt_access value.
279
280        Args:
281            always_use_jwt_access (bool): Whether always use self signed JWT or not.
282
283        Returns:
284            google.auth.service_account.Credentials: A new credentials
285                instance.
286        """
287        return self.__class__(
288            self._signer,
289            service_account_email=self._service_account_email,
290            scopes=self._scopes,
291            default_scopes=self._default_scopes,
292            token_uri=self._token_uri,
293            subject=self._subject,
294            project_id=self._project_id,
295            quota_project_id=self._quota_project_id,
296            additional_claims=self._additional_claims.copy(),
297            always_use_jwt_access=always_use_jwt_access,
298        )
299
300    def with_subject(self, subject):
301        """Create a copy of these credentials with the specified subject.
302
303        Args:
304            subject (str): The subject claim.
305
306        Returns:
307            google.auth.service_account.Credentials: A new credentials
308                instance.
309        """
310        return self.__class__(
311            self._signer,
312            service_account_email=self._service_account_email,
313            scopes=self._scopes,
314            default_scopes=self._default_scopes,
315            token_uri=self._token_uri,
316            subject=subject,
317            project_id=self._project_id,
318            quota_project_id=self._quota_project_id,
319            additional_claims=self._additional_claims.copy(),
320            always_use_jwt_access=self._always_use_jwt_access,
321        )
322
323    def with_claims(self, additional_claims):
324        """Returns a copy of these credentials with modified claims.
325
326        Args:
327            additional_claims (Mapping[str, str]): Any additional claims for
328                the JWT payload. This will be merged with the current
329                additional claims.
330
331        Returns:
332            google.auth.service_account.Credentials: A new credentials
333                instance.
334        """
335        new_additional_claims = copy.deepcopy(self._additional_claims)
336        new_additional_claims.update(additional_claims or {})
337
338        return self.__class__(
339            self._signer,
340            service_account_email=self._service_account_email,
341            scopes=self._scopes,
342            default_scopes=self._default_scopes,
343            token_uri=self._token_uri,
344            subject=self._subject,
345            project_id=self._project_id,
346            quota_project_id=self._quota_project_id,
347            additional_claims=new_additional_claims,
348            always_use_jwt_access=self._always_use_jwt_access,
349        )
350
351    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
352    def with_quota_project(self, quota_project_id):
353
354        return self.__class__(
355            self._signer,
356            service_account_email=self._service_account_email,
357            default_scopes=self._default_scopes,
358            scopes=self._scopes,
359            token_uri=self._token_uri,
360            subject=self._subject,
361            project_id=self._project_id,
362            quota_project_id=quota_project_id,
363            additional_claims=self._additional_claims.copy(),
364            always_use_jwt_access=self._always_use_jwt_access,
365        )
366
367    def _make_authorization_grant_assertion(self):
368        """Create the OAuth 2.0 assertion.
369
370        This assertion is used during the OAuth 2.0 grant to acquire an
371        access token.
372
373        Returns:
374            bytes: The authorization grant assertion.
375        """
376        now = _helpers.utcnow()
377        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
378        expiry = now + lifetime
379
380        payload = {
381            "iat": _helpers.datetime_to_secs(now),
382            "exp": _helpers.datetime_to_secs(expiry),
383            # The issuer must be the service account email.
384            "iss": self._service_account_email,
385            # The audience must be the auth token endpoint's URI
386            "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
387            "scope": _helpers.scopes_to_string(self._scopes or ()),
388        }
389
390        payload.update(self._additional_claims)
391
392        # The subject can be a user email for domain-wide delegation.
393        if self._subject:
394            payload.setdefault("sub", self._subject)
395
396        token = jwt.encode(self._signer, payload)
397
398        return token
399
400    @_helpers.copy_docstring(credentials.Credentials)
401    def refresh(self, request):
402        # Since domain wide delegation doesn't work with self signed JWT. If
403        # subject exists, then we should not use self signed JWT.
404        if self._subject is None and self._jwt_credentials is not None:
405            self._jwt_credentials.refresh(request)
406            self.token = self._jwt_credentials.token
407            self.expiry = self._jwt_credentials.expiry
408        else:
409            assertion = self._make_authorization_grant_assertion()
410            access_token, expiry, _ = _client.jwt_grant(
411                request, self._token_uri, assertion
412            )
413            self.token = access_token
414            self.expiry = expiry
415
416    def _create_self_signed_jwt(self, audience):
417        """Create a self-signed JWT from the credentials if requirements are met.
418
419        Args:
420            audience (str): The service URL. ``https://[API_ENDPOINT]/``
421        """
422        # https://google.aip.dev/auth/4111
423        if self._always_use_jwt_access:
424            if self._scopes:
425                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
426                    self, None, additional_claims={"scope": " ".join(self._scopes)}
427                )
428            elif audience:
429                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
430                    self, audience
431                )
432            elif self._default_scopes:
433                self._jwt_credentials = jwt.Credentials.from_signing_credentials(
434                    self,
435                    None,
436                    additional_claims={"scope": " ".join(self._default_scopes)},
437                )
438        elif not self._scopes and audience:
439            self._jwt_credentials = jwt.Credentials.from_signing_credentials(
440                self, audience
441            )
442
443    @_helpers.copy_docstring(credentials.Signing)
444    def sign_bytes(self, message):
445        return self._signer.sign(message)
446
447    @property
448    @_helpers.copy_docstring(credentials.Signing)
449    def signer(self):
450        return self._signer
451
452    @property
453    @_helpers.copy_docstring(credentials.Signing)
454    def signer_email(self):
455        return self._service_account_email
456
457
458class IDTokenCredentials(credentials.Signing, credentials.CredentialsWithQuotaProject):
459    """Open ID Connect ID Token-based service account credentials.
460
461    These credentials are largely similar to :class:`.Credentials`, but instead
462    of using an OAuth 2.0 Access Token as the bearer token, they use an Open
463    ID Connect ID Token as the bearer token. These credentials are useful when
464    communicating to services that require ID Tokens and can not accept access
465    tokens.
466
467    Usually, you'll create these credentials with one of the helper
468    constructors. To create credentials using a Google service account
469    private key JSON file::
470
471        credentials = (
472            service_account.IDTokenCredentials.from_service_account_file(
473                'service-account.json'))
474
475
476    Or if you already have the service account file loaded::
477
478        service_account_info = json.load(open('service_account.json'))
479        credentials = (
480            service_account.IDTokenCredentials.from_service_account_info(
481                service_account_info))
482
483
484    Both helper methods pass on arguments to the constructor, so you can
485    specify additional scopes and a subject if necessary::
486
487        credentials = (
488            service_account.IDTokenCredentials.from_service_account_file(
489                'service-account.json',
490                scopes=['email'],
491                subject='[email protected]'))
492
493
494    The credentials are considered immutable. If you want to modify the scopes
495    or the subject used for delegation, use :meth:`with_scopes` or
496    :meth:`with_subject`::
497
498        scoped_credentials = credentials.with_scopes(['email'])
499        delegated_credentials = credentials.with_subject(subject)
500
501    """
502
503    def __init__(
504        self,
505        signer,
506        service_account_email,
507        token_uri,
508        target_audience,
509        additional_claims=None,
510        quota_project_id=None,
511    ):
512        """
513        Args:
514            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
515            service_account_email (str): The service account's email.
516            token_uri (str): The OAuth 2.0 Token URI.
517            target_audience (str): The intended audience for these credentials,
518                used when requesting the ID Token. The ID Token's ``aud`` claim
519                will be set to this string.
520            additional_claims (Mapping[str, str]): Any additional claims for
521                the JWT assertion used in the authorization grant.
522            quota_project_id (Optional[str]): The project ID used for quota and billing.
523        .. note:: Typically one of the helper constructors
524            :meth:`from_service_account_file` or
525            :meth:`from_service_account_info` are used instead of calling the
526            constructor directly.
527        """
528        super(IDTokenCredentials, self).__init__()
529        self._signer = signer
530        self._service_account_email = service_account_email
531        self._token_uri = token_uri
532        self._target_audience = target_audience
533        self._quota_project_id = quota_project_id
534
535        if additional_claims is not None:
536            self._additional_claims = additional_claims
537        else:
538            self._additional_claims = {}
539
540    @classmethod
541    def _from_signer_and_info(cls, signer, info, **kwargs):
542        """Creates a credentials instance from a signer and service account
543        info.
544
545        Args:
546            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
547            info (Mapping[str, str]): The service account info.
548            kwargs: Additional arguments to pass to the constructor.
549
550        Returns:
551            google.auth.jwt.IDTokenCredentials: The constructed credentials.
552
553        Raises:
554            ValueError: If the info is not in the expected format.
555        """
556        kwargs.setdefault("service_account_email", info["client_email"])
557        kwargs.setdefault("token_uri", info["token_uri"])
558        return cls(signer, **kwargs)
559
560    @classmethod
561    def from_service_account_info(cls, info, **kwargs):
562        """Creates a credentials instance from parsed service account info.
563
564        Args:
565            info (Mapping[str, str]): The service account info in Google
566                format.
567            kwargs: Additional arguments to pass to the constructor.
568
569        Returns:
570            google.auth.service_account.IDTokenCredentials: The constructed
571                credentials.
572
573        Raises:
574            ValueError: If the info is not in the expected format.
575        """
576        signer = _service_account_info.from_dict(
577            info, require=["client_email", "token_uri"]
578        )
579        return cls._from_signer_and_info(signer, info, **kwargs)
580
581    @classmethod
582    def from_service_account_file(cls, filename, **kwargs):
583        """Creates a credentials instance from a service account json file.
584
585        Args:
586            filename (str): The path to the service account json file.
587            kwargs: Additional arguments to pass to the constructor.
588
589        Returns:
590            google.auth.service_account.IDTokenCredentials: The constructed
591                credentials.
592        """
593        info, signer = _service_account_info.from_filename(
594            filename, require=["client_email", "token_uri"]
595        )
596        return cls._from_signer_and_info(signer, info, **kwargs)
597
598    def with_target_audience(self, target_audience):
599        """Create a copy of these credentials with the specified target
600        audience.
601
602        Args:
603            target_audience (str): The intended audience for these credentials,
604            used when requesting the ID Token.
605
606        Returns:
607            google.auth.service_account.IDTokenCredentials: A new credentials
608                instance.
609        """
610        return self.__class__(
611            self._signer,
612            service_account_email=self._service_account_email,
613            token_uri=self._token_uri,
614            target_audience=target_audience,
615            additional_claims=self._additional_claims.copy(),
616            quota_project_id=self.quota_project_id,
617        )
618
619    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
620    def with_quota_project(self, quota_project_id):
621        return self.__class__(
622            self._signer,
623            service_account_email=self._service_account_email,
624            token_uri=self._token_uri,
625            target_audience=self._target_audience,
626            additional_claims=self._additional_claims.copy(),
627            quota_project_id=quota_project_id,
628        )
629
630    def _make_authorization_grant_assertion(self):
631        """Create the OAuth 2.0 assertion.
632
633        This assertion is used during the OAuth 2.0 grant to acquire an
634        ID token.
635
636        Returns:
637            bytes: The authorization grant assertion.
638        """
639        now = _helpers.utcnow()
640        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
641        expiry = now + lifetime
642
643        payload = {
644            "iat": _helpers.datetime_to_secs(now),
645            "exp": _helpers.datetime_to_secs(expiry),
646            # The issuer must be the service account email.
647            "iss": self.service_account_email,
648            # The audience must be the auth token endpoint's URI
649            "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
650            # The target audience specifies which service the ID token is
651            # intended for.
652            "target_audience": self._target_audience,
653        }
654
655        payload.update(self._additional_claims)
656
657        token = jwt.encode(self._signer, payload)
658
659        return token
660
661    @_helpers.copy_docstring(credentials.Credentials)
662    def refresh(self, request):
663        assertion = self._make_authorization_grant_assertion()
664        access_token, expiry, _ = _client.id_token_jwt_grant(
665            request, self._token_uri, assertion
666        )
667        self.token = access_token
668        self.expiry = expiry
669
670    @property
671    def service_account_email(self):
672        """The service account email."""
673        return self._service_account_email
674
675    @_helpers.copy_docstring(credentials.Signing)
676    def sign_bytes(self, message):
677        return self._signer.sign(message)
678
679    @property
680    @_helpers.copy_docstring(credentials.Signing)
681    def signer(self):
682        return self._signer
683
684    @property
685    @_helpers.copy_docstring(credentials.Signing)
686    def signer_email(self):
687        return self._service_account_email
688