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 ID Token helpers.
16
17Provides support for verifying `OpenID Connect ID Tokens`_, especially ones
18generated by Google infrastructure.
19
20To parse and verify an ID Token issued by Google's OAuth 2.0 authorization
21server use :func:`verify_oauth2_token`. To verify an ID Token issued by
22Firebase, use :func:`verify_firebase_token`.
23
24A general purpose ID Token verifier is available as :func:`verify_token`.
25
26Example::
27
28    from google.oauth2 import id_token
29    from google.auth.transport import requests
30
31    request = requests.Request()
32
33    id_info = id_token.verify_oauth2_token(
34        token, request, 'my-client-id.example.com')
35
36    userid = id_info['sub']
37
38By default, this will re-fetch certificates for each verification. Because
39Google's public keys are only changed infrequently (on the order of once per
40day), you may wish to take advantage of caching to reduce latency and the
41potential for network errors. This can be accomplished using an external
42library like `CacheControl`_ to create a cache-aware
43:class:`google.auth.transport.Request`::
44
45    import cachecontrol
46    import google.auth.transport.requests
47    import requests
48
49    session = requests.session()
50    cached_session = cachecontrol.CacheControl(session)
51    request = google.auth.transport.requests.Request(session=cached_session)
52
53.. _OpenID Connect ID Tokens:
54    http://openid.net/specs/openid-connect-core-1_0.html#IDToken
55.. _CacheControl: https://cachecontrol.readthedocs.io
56"""
57
58import json
59import os
60
61import six
62from six.moves import http_client
63
64from google.auth import environment_vars
65from google.auth import exceptions
66from google.auth import jwt
67import google.auth.transport.requests
68
69
70# The URL that provides public certificates for verifying ID tokens issued
71# by Google's OAuth 2.0 authorization server.
72_GOOGLE_OAUTH2_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs"
73
74# The URL that provides public certificates for verifying ID tokens issued
75# by Firebase and the Google APIs infrastructure
76_GOOGLE_APIS_CERTS_URL = (
77    "https://www.googleapis.com/robot/v1/metadata/x509"
78    "/[email protected]"
79)
80
81_GOOGLE_ISSUERS = ["accounts.google.com", "https://accounts.google.com"]
82
83
84def _fetch_certs(request, certs_url):
85    """Fetches certificates.
86
87    Google-style cerificate endpoints return JSON in the format of
88    ``{'key id': 'x509 certificate'}``.
89
90    Args:
91        request (google.auth.transport.Request): The object used to make
92            HTTP requests.
93        certs_url (str): The certificate endpoint URL.
94
95    Returns:
96        Mapping[str, str]: A mapping of public key ID to x.509 certificate
97            data.
98    """
99    response = request(certs_url, method="GET")
100
101    if response.status != http_client.OK:
102        raise exceptions.TransportError(
103            "Could not fetch certificates at {}".format(certs_url)
104        )
105
106    return json.loads(response.data.decode("utf-8"))
107
108
109def verify_token(
110    id_token,
111    request,
112    audience=None,
113    certs_url=_GOOGLE_OAUTH2_CERTS_URL,
114    clock_skew_in_seconds=0,
115):
116    """Verifies an ID token and returns the decoded token.
117
118    Args:
119        id_token (Union[str, bytes]): The encoded token.
120        request (google.auth.transport.Request): The object used to make
121            HTTP requests.
122        audience (str or list): The audience or audiences that this token is
123            intended for. If None then the audience is not verified.
124        certs_url (str): The URL that specifies the certificates to use to
125            verify the token. This URL should return JSON in the format of
126            ``{'key id': 'x509 certificate'}``.
127        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
128            validation.
129
130    Returns:
131        Mapping[str, Any]: The decoded token.
132    """
133    certs = _fetch_certs(request, certs_url)
134
135    return jwt.decode(
136        id_token,
137        certs=certs,
138        audience=audience,
139        clock_skew_in_seconds=clock_skew_in_seconds,
140    )
141
142
143def verify_oauth2_token(id_token, request, audience=None, clock_skew_in_seconds=0):
144    """Verifies an ID Token issued by Google's OAuth 2.0 authorization server.
145
146    Args:
147        id_token (Union[str, bytes]): The encoded token.
148        request (google.auth.transport.Request): The object used to make
149            HTTP requests.
150        audience (str): The audience that this token is intended for. This is
151            typically your application's OAuth 2.0 client ID. If None then the
152            audience is not verified.
153        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
154            validation.
155
156    Returns:
157        Mapping[str, Any]: The decoded token.
158
159    Raises:
160        exceptions.GoogleAuthError: If the issuer is invalid.
161    """
162    idinfo = verify_token(
163        id_token,
164        request,
165        audience=audience,
166        certs_url=_GOOGLE_OAUTH2_CERTS_URL,
167        clock_skew_in_seconds=clock_skew_in_seconds,
168    )
169
170    if idinfo["iss"] not in _GOOGLE_ISSUERS:
171        raise exceptions.GoogleAuthError(
172            "Wrong issuer. 'iss' should be one of the following: {}".format(
173                _GOOGLE_ISSUERS
174            )
175        )
176
177    return idinfo
178
179
180def verify_firebase_token(id_token, request, audience=None, clock_skew_in_seconds=0):
181    """Verifies an ID Token issued by Firebase Authentication.
182
183    Args:
184        id_token (Union[str, bytes]): The encoded token.
185        request (google.auth.transport.Request): The object used to make
186            HTTP requests.
187        audience (str): The audience that this token is intended for. This is
188            typically your Firebase application ID. If None then the audience
189            is not verified.
190        clock_skew_in_seconds (int): The clock skew used for `iat` and `exp`
191            validation.
192
193    Returns:
194        Mapping[str, Any]: The decoded token.
195    """
196    return verify_token(
197        id_token,
198        request,
199        audience=audience,
200        certs_url=_GOOGLE_APIS_CERTS_URL,
201        clock_skew_in_seconds=clock_skew_in_seconds,
202    )
203
204
205def fetch_id_token_credentials(audience, request=None):
206    """Create the ID Token credentials from the current environment.
207
208    This function acquires ID token from the environment in the following order.
209    See https://google.aip.dev/auth/4110.
210
211    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
212       to the path of a valid service account JSON file, then ID token is
213       acquired using this service account credentials.
214    2. If the application is running in Compute Engine, App Engine or Cloud Run,
215       then the ID token are obtained from the metadata server.
216    3. If metadata server doesn't exist and no valid service account credentials
217       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
218       be raised.
219
220    Example::
221
222        import google.oauth2.id_token
223        import google.auth.transport.requests
224
225        request = google.auth.transport.requests.Request()
226        target_audience = "https://pubsub.googleapis.com"
227
228        # Create ID token credentials.
229        credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request)
230
231        # Refresh the credential to obtain an ID token.
232        credentials.refresh(request)
233
234        id_token = credentials.token
235        id_token_expiry = credentials.expiry
236
237    Args:
238        audience (str): The audience that this ID token is intended for.
239        request (Optional[google.auth.transport.Request]): A callable used to make
240            HTTP requests. A request object will be created if not provided.
241
242    Returns:
243        google.auth.credentials.Credentials: The ID token credentials.
244
245    Raises:
246        ~google.auth.exceptions.DefaultCredentialsError:
247            If metadata server doesn't exist and no valid service account
248            credentials are found.
249    """
250    # 1. Try to get credentials from the GOOGLE_APPLICATION_CREDENTIALS environment
251    # variable.
252    credentials_filename = os.environ.get(environment_vars.CREDENTIALS)
253    if credentials_filename:
254        if not (
255            os.path.exists(credentials_filename)
256            and os.path.isfile(credentials_filename)
257        ):
258            raise exceptions.DefaultCredentialsError(
259                "GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid."
260            )
261
262        try:
263            with open(credentials_filename, "r") as f:
264                from google.oauth2 import service_account
265
266                info = json.load(f)
267                if info.get("type") == "service_account":
268                    return service_account.IDTokenCredentials.from_service_account_info(
269                        info, target_audience=audience
270                    )
271        except ValueError as caught_exc:
272            new_exc = exceptions.DefaultCredentialsError(
273                "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.",
274                caught_exc,
275            )
276            six.raise_from(new_exc, caught_exc)
277
278    # 2. Try to fetch ID token from metada server if it exists. The code
279    # works for GAE and Cloud Run metadata server as well.
280    try:
281        from google.auth import compute_engine
282        from google.auth.compute_engine import _metadata
283
284        # Create a request object if not provided.
285        if not request:
286            request = google.auth.transport.requests.Request()
287
288        if _metadata.ping(request):
289            return compute_engine.IDTokenCredentials(
290                request, audience, use_metadata_identity_endpoint=True
291            )
292    except (ImportError, exceptions.TransportError):
293        pass
294
295    raise exceptions.DefaultCredentialsError(
296        "Neither metadata server or valid service account credentials are found."
297    )
298
299
300def fetch_id_token(request, audience):
301    """Fetch the ID Token from the current environment.
302
303    This function acquires ID token from the environment in the following order.
304    See https://google.aip.dev/auth/4110.
305
306    1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set
307       to the path of a valid service account JSON file, then ID token is
308       acquired using this service account credentials.
309    2. If the application is running in Compute Engine, App Engine or Cloud Run,
310       then the ID token are obtained from the metadata server.
311    3. If metadata server doesn't exist and no valid service account credentials
312       are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will
313       be raised.
314
315    Example::
316
317        import google.oauth2.id_token
318        import google.auth.transport.requests
319
320        request = google.auth.transport.requests.Request()
321        target_audience = "https://pubsub.googleapis.com"
322
323        id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)
324
325    Args:
326        request (google.auth.transport.Request): A callable used to make
327            HTTP requests.
328        audience (str): The audience that this ID token is intended for.
329
330    Returns:
331        str: The ID token.
332
333    Raises:
334        ~google.auth.exceptions.DefaultCredentialsError:
335            If metadata server doesn't exist and no valid service account
336            credentials are found.
337    """
338    id_token_credentials = fetch_id_token_credentials(audience, request=request)
339    id_token_credentials.refresh(request)
340    return id_token_credentials.token
341