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