Skip to content

Instantly share code, notes, and snippets.

@iAnanich
Created October 22, 2020 07:11
Show Gist options
  • Save iAnanich/851a4e757a15e50d03225480f667a486 to your computer and use it in GitHub Desktop.
Save iAnanich/851a4e757a15e50d03225480f667a486 to your computer and use it in GitHub Desktop.
Google's ID token verification module
"""
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