Created
October 22, 2020 07:11
-
-
Save iAnanich/851a4e757a15e50d03225480f667a486 to your computer and use it in GitHub Desktop.
Google's ID token verification module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
https://developers.google.com/identity/sign-in/android/backend-auth#verify-the-integrity-of-the-id-token | |
""" | |
import json | |
from typing import Optional, List | |
import yarl | |
from google.oauth2 import id_token | |
from google.auth.transport import requests as google_requests | |
import requests | |
if not google_requests.requests is requests: | |
raise RuntimeError('Google API Client library changed transport library!') | |
class IdTokenInvalid(ValueError): | |
pass | |
class IdTokenAudienceMismatch(IdTokenInvalid): | |
pass | |
def verify_idtoken_locally(token: str, | |
client_id: Optional[str] = None, | |
*, request: Optional[google_requests.Request] = None) -> dict: | |
""" | |
https://developers.google.com/identity/sign-in/android/backend-auth#using-a-google-api-client-library | |
:param token: Google ID token | |
:param client_id: your app's Client ID, optional | |
:return: | |
""" | |
if request is None: | |
request = google_requests.Request() | |
payload = id_token.verify_oauth2_token( | |
id_token=token, | |
request=request, | |
audience=client_id, | |
) | |
return payload | |
def verify_idtoken_by_google(token: str, | |
client_id: Optional[str] = None, | |
*, request: Optional[google_requests.Request] = None) -> dict: | |
""" | |
https://developers.google.com/identity/sign-in/android/backend-auth#calling-the-tokeninfo-endpoint | |
:param token: Google ID token | |
:param client_id: your app's Client ID, optional | |
:return: | |
""" | |
if request is None: | |
request = google_requests.Request() | |
url = yarl.URL('https://oauth2.googleapis.com/tokeninfo').with_query({ | |
'id_token': token, | |
}) | |
response = request(url=str(url)) | |
payload = json.loads(response.data.decode()) | |
if response.status != 200: | |
raise ValueError( | |
f'{"".join(payload["error"].title().split("_"))}Error: "{payload["error_description"]}"' | |
) | |
# check Client ID from token with passed Client ID | |
if client_id is not None: | |
claim_audience = payload.get('aud') | |
if client_id != claim_audience: | |
raise ValueError( | |
f'Token has wrong audience {claim_audience}, expected {client_id}' | |
) | |
return payload | |
def verify_idtoken(token: str, | |
client_id: Optional[str] = None, | |
allowed_audience: Optional[List[str]] = None, | |
*, verify_by_google: bool = False, | |
request: Optional[google_requests.Request] = None) -> dict: | |
try: | |
if verify_by_google: | |
payload = verify_idtoken_by_google(token=token, client_id=client_id, request=request) | |
else: | |
payload = verify_idtoken_locally(token=token, client_id=client_id, request=request) | |
except ValueError as exc: | |
raise IdTokenInvalid from exc | |
else: | |
if not client_id and allowed_audience: | |
# check Client ID from token with set of allowed Client IDs | |
token_audience = payload['aud'] | |
if token_audience not in set(allowed_audience): | |
raise IdTokenAudienceMismatch( | |
f'Token issued for audience "{token_audience}", but it was ' | |
f'not included in allowed audience: {", ".join(allowed_audience)}.' | |
) | |
return payload | |
class IdTokenVerifier: | |
class DEFAULT: | |
VERIFY_BY_GOOGLE = False | |
""" It's better to decode and verify JWT token locally because | |
requested to Google might get throttled and stuck. """ | |
def __init__(self, client_id: Optional[str] = None, | |
allowed_audience: Optional[List[str]] = None, | |
verify_by_google: bool = DEFAULT.VERIFY_BY_GOOGLE): | |
self.default_client_id = str(client_id) if client_id else None | |
self.default_allowed_audience = tuple(allowed_audience) if allowed_audience else None | |
self.default_verify_by_google = bool(verify_by_google) | |
self.session = requests.Session() | |
def verify_idtoken(self, token: str, | |
client_id: Optional[str] = None, | |
allowed_audience: Optional[List[str]] = None, | |
*, verify_by_google: Optional[bool] = None) -> dict: | |
return verify_idtoken( | |
token=str(token), | |
client_id=str(client_id) if client_id else self.default_client_id, | |
allowed_audience=allowed_audience or self.default_allowed_audience, | |
verify_by_google=bool(verify_by_google) if verify_by_google is not None else self.default_verify_by_google, | |
request=google_requests.Request(session=self.session), | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment