1515"""Firebase App Check module."""
1616
1717from typing import Any , Dict
18+ import requests
1819import jwt
1920from jwt import PyJWKClient , ExpiredSignatureError , InvalidTokenError , DecodeError
2021from 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
2526def _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
4551class _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