Skip to content

Commit 3e328ce

Browse files
committed
add one time token verification support
1 parent 62e559b commit 3e328ce

1 file changed

Lines changed: 29 additions & 6 deletions

File tree

firebase_admin/app_check.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,47 @@
1515
"""Firebase App Check module."""
1616

1717
from typing import Any, Dict
18+
import requests
1819
import jwt
1920
from jwt import PyJWKClient, ExpiredSignatureError, InvalidTokenError, DecodeError
2021
from jwt import InvalidAudienceError, InvalidIssuerError, InvalidSignatureError
21-
from firebase_admin import _utils
22+
from firebase_admin import _http_client, _utils
2223

2324
_APP_CHECK_ATTRIBUTE = '_app_check'
2425

2526
def _get_app_check_service(app) -> Any:
2627
return _utils.get_app_service(app, _APP_CHECK_ATTRIBUTE, _AppCheckService)
2728

28-
def verify_token(token: str, app=None) -> Dict[str, Any]:
29+
def verify_token(token: str, app=None, consume: bool = False) -> Dict[str, Any]:
2930
"""Verifies a Firebase App Check token.
3031
3132
Args:
3233
token: A token from App Check.
3334
app: An App instance (optional).
35+
consume: If set to ``True``, performs stateful verification with the App Check
36+
backend to mark the token as consumed for replay protection. Defaults to ``False``.
3437
3538
Returns:
36-
Dict[str, Any]: The token's decoded claims.
39+
Dict[str, Any]: The token's decoded claims. If ``consume`` is ``True``, the dictionary
40+
also includes an ``already_consumed`` boolean key indicating whether the token was
41+
previously consumed.
3742
3843
Raises:
3944
ValueError: If the app's ``project_id`` is invalid or unspecified,
40-
or if the token's headers or payload are invalid.
45+
or if the token's headers or payload are invalid.
46+
FirebaseError: If an error occurs while communicating with the App Check service.
4147
PyJWKClientError: If PyJWKClient fails to fetch a valid signing key.
4248
"""
43-
return _get_app_check_service(app).verify_token(token)
49+
return _get_app_check_service(app).verify_token(token, consume=consume)
4450

4551
class _AppCheckService:
4652
"""Service class that implements Firebase App Check functionality."""
4753

4854
_APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'
4955
_JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks'
56+
_VERIFY_URL_FORMAT = (
57+
'https://firebaseappcheck.googleapis.com/v1/projects/{project_id}:verifyAppCheckToken'
58+
)
5059
_project_id = None
5160
_scoped_project_id = None
5261
_jwks_client = None
@@ -68,9 +77,12 @@ def __init__(self, app):
6877
# Default lifespan is 300 seconds (5 minutes) so we change it to 21600 seconds (6 hours).
6978
self._jwks_client = PyJWKClient(
7079
self._JWKS_URL, lifespan=21600, headers=self._APP_CHECK_HEADERS)
80+
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
81+
self._http_client = _http_client.JsonHttpClient(
82+
credential=app.credential.get_credential(), timeout=timeout)
7183

7284

73-
def verify_token(self, token: str) -> Dict[str, Any]:
85+
def verify_token(self, token: str, consume: bool = False) -> Dict[str, Any]:
7486
"""Verifies a Firebase App Check token."""
7587
_Validators.check_string("app check token", token)
7688

@@ -87,6 +99,17 @@ def verify_token(self, token: str) -> Dict[str, Any]:
8799
) from exception
88100

89101
verified_claims['app_id'] = verified_claims.get('sub')
102+
103+
if consume:
104+
url = self._VERIFY_URL_FORMAT.format(project_id=self._project_id)
105+
try:
106+
body = self._http_client.body('post', url, json={'app_check_token': token})
107+
except requests.exceptions.RequestException as error:
108+
raise _utils.handle_requests_error(error)
109+
110+
already_consumed = body.get('alreadyConsumed', False) if isinstance(body, dict) else False
111+
verified_claims['already_consumed'] = bool(already_consumed)
112+
90113
return verified_claims
91114

92115
def _has_valid_token_headers(self, headers: Any) -> None:

0 commit comments

Comments
 (0)