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"""Google Compute Engine credentials.
16
17This module provides authentication for an application running on Google
18Compute Engine using the Compute Engine metadata server.
19
20"""
21
22import datetime
23
24import six
25
26from google.auth import _helpers
27from google.auth import credentials
28from google.auth import exceptions
29from google.auth import iam
30from google.auth import jwt
31from google.auth.compute_engine import _metadata
32from google.oauth2 import _client
33
34
35class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
36    """Compute Engine Credentials.
37
38    These credentials use the Google Compute Engine metadata server to obtain
39    OAuth 2.0 access tokens associated with the instance's service account,
40    and are also used for Cloud Run, Flex and App Engine (except for the Python
41    2.7 runtime, which is supported only on older versions of this library).
42
43    For more information about Compute Engine authentication, including how
44    to configure scopes, see the `Compute Engine authentication
45    documentation`_.
46
47    .. note:: On Compute Engine the metadata server ignores requested scopes.
48        On Cloud Run, Flex and App Engine the server honours requested scopes.
49
50    .. _Compute Engine authentication documentation:
51        https://cloud.google.com/compute/docs/authentication#using
52    """
53
54    def __init__(
55        self,
56        service_account_email="default",
57        quota_project_id=None,
58        scopes=None,
59        default_scopes=None,
60    ):
61        """
62        Args:
63            service_account_email (str): The service account email to use, or
64                'default'. A Compute Engine instance may have multiple service
65                accounts.
66            quota_project_id (Optional[str]): The project ID used for quota and
67                billing.
68            scopes (Optional[Sequence[str]]): The list of scopes for the credentials.
69            default_scopes (Optional[Sequence[str]]): Default scopes passed by a
70                Google client library. Use 'scopes' for user-defined scopes.
71        """
72        super(Credentials, self).__init__()
73        self._service_account_email = service_account_email
74        self._quota_project_id = quota_project_id
75        self._scopes = scopes
76        self._default_scopes = default_scopes
77
78    def _retrieve_info(self, request):
79        """Retrieve information about the service account.
80
81        Updates the scopes and retrieves the full service account email.
82
83        Args:
84            request (google.auth.transport.Request): The object used to make
85                HTTP requests.
86        """
87        info = _metadata.get_service_account_info(
88            request, service_account=self._service_account_email
89        )
90
91        self._service_account_email = info["email"]
92
93        # Don't override scopes requested by the user.
94        if self._scopes is None:
95            self._scopes = info["scopes"]
96
97    def refresh(self, request):
98        """Refresh the access token and scopes.
99
100        Args:
101            request (google.auth.transport.Request): The object used to make
102                HTTP requests.
103
104        Raises:
105            google.auth.exceptions.RefreshError: If the Compute Engine metadata
106                service can't be reached if if the instance has not
107                credentials.
108        """
109        scopes = self._scopes if self._scopes is not None else self._default_scopes
110        try:
111            self._retrieve_info(request)
112            self.token, self.expiry = _metadata.get_service_account_token(
113                request, service_account=self._service_account_email, scopes=scopes
114            )
115        except exceptions.TransportError as caught_exc:
116            new_exc = exceptions.RefreshError(caught_exc)
117            six.raise_from(new_exc, caught_exc)
118
119    @property
120    def service_account_email(self):
121        """The service account email.
122
123        .. note:: This is not guaranteed to be set until :meth:`refresh` has been
124            called.
125        """
126        return self._service_account_email
127
128    @property
129    def requires_scopes(self):
130        return not self._scopes
131
132    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
133    def with_quota_project(self, quota_project_id):
134        return self.__class__(
135            service_account_email=self._service_account_email,
136            quota_project_id=quota_project_id,
137            scopes=self._scopes,
138        )
139
140    @_helpers.copy_docstring(credentials.Scoped)
141    def with_scopes(self, scopes, default_scopes=None):
142        # Compute Engine credentials can not be scoped (the metadata service
143        # ignores the scopes parameter). App Engine, Cloud Run and Flex support
144        # requesting scopes.
145        return self.__class__(
146            scopes=scopes,
147            default_scopes=default_scopes,
148            service_account_email=self._service_account_email,
149            quota_project_id=self._quota_project_id,
150        )
151
152
153_DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
154_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
155
156
157class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing):
158    """Open ID Connect ID Token-based service account credentials.
159
160    These credentials relies on the default service account of a GCE instance.
161
162    ID token can be requested from `GCE metadata server identity endpoint`_, IAM
163    token endpoint or other token endpoints you specify. If metadata server
164    identity endpoint is not used, the GCE instance must have been started with
165    a service account that has access to the IAM Cloud API.
166
167    .. _GCE metadata server identity endpoint:
168        https://cloud.google.com/compute/docs/instances/verifying-instance-identity
169    """
170
171    def __init__(
172        self,
173        request,
174        target_audience,
175        token_uri=None,
176        additional_claims=None,
177        service_account_email=None,
178        signer=None,
179        use_metadata_identity_endpoint=False,
180        quota_project_id=None,
181    ):
182        """
183        Args:
184            request (google.auth.transport.Request): The object used to make
185                HTTP requests.
186            target_audience (str): The intended audience for these credentials,
187                used when requesting the ID Token. The ID Token's ``aud`` claim
188                will be set to this string.
189            token_uri (str): The OAuth 2.0 Token URI.
190            additional_claims (Mapping[str, str]): Any additional claims for
191                the JWT assertion used in the authorization grant.
192            service_account_email (str): Optional explicit service account to
193                use to sign JWT tokens.
194                By default, this is the default GCE service account.
195            signer (google.auth.crypt.Signer): The signer used to sign JWTs.
196                In case the signer is specified, the request argument will be
197                ignored.
198            use_metadata_identity_endpoint (bool): Whether to use GCE metadata
199                identity endpoint. For backward compatibility the default value
200                is False. If set to True, ``token_uri``, ``additional_claims``,
201                ``service_account_email``, ``signer`` argument should not be set;
202                otherwise ValueError will be raised.
203            quota_project_id (Optional[str]): The project ID used for quota and
204                billing.
205
206        Raises:
207            ValueError:
208                If ``use_metadata_identity_endpoint`` is set to True, and one of
209                ``token_uri``, ``additional_claims``, ``service_account_email``,
210                 ``signer`` arguments is set.
211        """
212        super(IDTokenCredentials, self).__init__()
213
214        self._quota_project_id = quota_project_id
215        self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
216        self._target_audience = target_audience
217
218        if use_metadata_identity_endpoint:
219            if token_uri or additional_claims or service_account_email or signer:
220                raise ValueError(
221                    "If use_metadata_identity_endpoint is set, token_uri, "
222                    "additional_claims, service_account_email, signer arguments"
223                    " must not be set"
224                )
225            self._token_uri = None
226            self._additional_claims = None
227            self._signer = None
228
229        if service_account_email is None:
230            sa_info = _metadata.get_service_account_info(request)
231            self._service_account_email = sa_info["email"]
232        else:
233            self._service_account_email = service_account_email
234
235        if not use_metadata_identity_endpoint:
236            if signer is None:
237                signer = iam.Signer(
238                    request=request,
239                    credentials=Credentials(),
240                    service_account_email=self._service_account_email,
241                )
242            self._signer = signer
243            self._token_uri = token_uri or _DEFAULT_TOKEN_URI
244
245            if additional_claims is not None:
246                self._additional_claims = additional_claims
247            else:
248                self._additional_claims = {}
249
250    def with_target_audience(self, target_audience):
251        """Create a copy of these credentials with the specified target
252        audience.
253        Args:
254            target_audience (str): The intended audience for these credentials,
255            used when requesting the ID Token.
256        Returns:
257            google.auth.service_account.IDTokenCredentials: A new credentials
258                instance.
259        """
260        # since the signer is already instantiated,
261        # the request is not needed
262        if self._use_metadata_identity_endpoint:
263            return self.__class__(
264                None,
265                target_audience=target_audience,
266                use_metadata_identity_endpoint=True,
267                quota_project_id=self._quota_project_id,
268            )
269        else:
270            return self.__class__(
271                None,
272                service_account_email=self._service_account_email,
273                token_uri=self._token_uri,
274                target_audience=target_audience,
275                additional_claims=self._additional_claims.copy(),
276                signer=self.signer,
277                use_metadata_identity_endpoint=False,
278                quota_project_id=self._quota_project_id,
279            )
280
281    @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
282    def with_quota_project(self, quota_project_id):
283
284        # since the signer is already instantiated,
285        # the request is not needed
286        if self._use_metadata_identity_endpoint:
287            return self.__class__(
288                None,
289                target_audience=self._target_audience,
290                use_metadata_identity_endpoint=True,
291                quota_project_id=quota_project_id,
292            )
293        else:
294            return self.__class__(
295                None,
296                service_account_email=self._service_account_email,
297                token_uri=self._token_uri,
298                target_audience=self._target_audience,
299                additional_claims=self._additional_claims.copy(),
300                signer=self.signer,
301                use_metadata_identity_endpoint=False,
302                quota_project_id=quota_project_id,
303            )
304
305    def _make_authorization_grant_assertion(self):
306        """Create the OAuth 2.0 assertion.
307        This assertion is used during the OAuth 2.0 grant to acquire an
308        ID token.
309        Returns:
310            bytes: The authorization grant assertion.
311        """
312        now = _helpers.utcnow()
313        lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
314        expiry = now + lifetime
315
316        payload = {
317            "iat": _helpers.datetime_to_secs(now),
318            "exp": _helpers.datetime_to_secs(expiry),
319            # The issuer must be the service account email.
320            "iss": self.service_account_email,
321            # The audience must be the auth token endpoint's URI
322            "aud": self._token_uri,
323            # The target audience specifies which service the ID token is
324            # intended for.
325            "target_audience": self._target_audience,
326        }
327
328        payload.update(self._additional_claims)
329
330        token = jwt.encode(self._signer, payload)
331
332        return token
333
334    def _call_metadata_identity_endpoint(self, request):
335        """Request ID token from metadata identity endpoint.
336
337        Args:
338            request (google.auth.transport.Request): The object used to make
339                HTTP requests.
340
341        Returns:
342            Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token.
343
344        Raises:
345            google.auth.exceptions.RefreshError: If the Compute Engine metadata
346                service can't be reached or if the instance has no credentials.
347            ValueError: If extracting expiry from the obtained ID token fails.
348        """
349        try:
350            path = "instance/service-accounts/default/identity"
351            params = {"audience": self._target_audience, "format": "full"}
352            id_token = _metadata.get(request, path, params=params)
353        except exceptions.TransportError as caught_exc:
354            new_exc = exceptions.RefreshError(caught_exc)
355            six.raise_from(new_exc, caught_exc)
356
357        _, payload, _, _ = jwt._unverified_decode(id_token)
358        return id_token, datetime.datetime.fromtimestamp(payload["exp"])
359
360    def refresh(self, request):
361        """Refreshes the ID token.
362
363        Args:
364            request (google.auth.transport.Request): The object used to make
365                HTTP requests.
366
367        Raises:
368            google.auth.exceptions.RefreshError: If the credentials could
369                not be refreshed.
370            ValueError: If extracting expiry from the obtained ID token fails.
371        """
372        if self._use_metadata_identity_endpoint:
373            self.token, self.expiry = self._call_metadata_identity_endpoint(request)
374        else:
375            assertion = self._make_authorization_grant_assertion()
376            access_token, expiry, _ = _client.id_token_jwt_grant(
377                request, self._token_uri, assertion
378            )
379            self.token = access_token
380            self.expiry = expiry
381
382    @property
383    @_helpers.copy_docstring(credentials.Signing)
384    def signer(self):
385        return self._signer
386
387    def sign_bytes(self, message):
388        """Signs the given message.
389
390        Args:
391            message (bytes): The message to sign.
392
393        Returns:
394            bytes: The message's cryptographic signature.
395
396        Raises:
397            ValueError:
398                Signer is not available if metadata identity endpoint is used.
399        """
400        if self._use_metadata_identity_endpoint:
401            raise ValueError(
402                "Signer is not available if metadata identity endpoint is used"
403            )
404        return self._signer.sign(message)
405
406    @property
407    def service_account_email(self):
408        """The service account email."""
409        return self._service_account_email
410
411    @property
412    def signer_email(self):
413        return self._service_account_email
414