1717This module contains functions for verifying JWTs related to the Firebase
1818Phone Number Verification (FPNV) service.
1919"""
20- from typing import Any , Dict
20+ from __future__ import annotations
21+ from typing import Any , Dict , Optional
2122
2223import jwt
23- from jwt import PyJWKClient , InvalidTokenError , DecodeError , InvalidSignatureError , \
24+ from jwt import (
25+ PyJWKClient , InvalidTokenError , DecodeError , InvalidSignatureError ,
2426 PyJWKClientError , InvalidAudienceError , InvalidIssuerError , ExpiredSignatureError
27+ )
2528
26- from firebase_admin import _utils
29+ from firebase_admin import App , _utils , exceptions
2730
2831_FPNV_ATTRIBUTE = '_fpnv'
2932_FPNV_JWKS_URL = 'https://fpnv.googleapis.com/v1beta/jwks'
3033_FPNV_ISSUER = 'https://fpnv.googleapis.com/projects/'
3134_ALGORITHM_ES256 = 'ES256'
3235
3336
34- def client (app = None ):
35- """Returns an instance of the FPNV service for the specified app.
37+ def _get_fpnv_service (app ):
38+ return _utils .get_app_service (app , _FPNV_ATTRIBUTE , _FpnvService )
39+
40+ def verify_token (token : str , app : Optional [App ] = None ) -> FpnvToken :
41+ """Verifies a Firebase Phone Number Verification (FPNV) token.
3642
3743 Args:
44+ token: A string containing the FPNV JWT.
3845 app: An App instance (optional).
3946
4047 Returns:
41- FpnvClient: A FpnvClient instance .
48+ FpnvToken: The verified token claims .
4249
4350 Raises:
44- ValueError: If the app is not a valid App instance.
51+ InvalidFpnvTokenError: If the token is invalid or malformed.
52+ ExpiredFpnvTokenError: If the token has expired.
4553 """
46- return _utils . get_app_service (app , _FPNV_ATTRIBUTE , FpnvClient )
54+ return _get_fpnv_service (app ). verify_token ( token )
4755
4856
4957class FpnvToken (dict ):
@@ -55,36 +63,37 @@ class FpnvToken(dict):
5563
5664 def __init__ (self , claims ):
5765 super ().__init__ (claims )
66+ self ['phone_number' ] = claims .get ('sub' )
5867
5968 @property
60- def phone_number (self ):
69+ def phone_number (self ) -> str :
6170 """Returns the phone number of the user.
6271 This corresponds to the 'sub' claim in the JWT.
6372 """
6473 return self .get ('sub' )
6574
6675 @property
67- def issuer (self ):
76+ def issuer (self ) -> str :
6877 """Returns the issuer identifier for the issuer of the response."""
6978 return self .get ('iss' )
7079
7180 @property
72- def audience (self ):
81+ def audience (self ) -> str :
7382 """Returns the audience for which this token is intended."""
7483 return self .get ('aud' )
7584
7685 @property
77- def exp (self ):
86+ def exp (self ) -> int :
7887 """Returns the expiration time since the Unix epoch."""
7988 return self .get ('exp' )
8089
8190 @property
82- def iat (self ):
91+ def iat (self ) -> int :
8392 """Returns the issued-at time since the Unix epoch."""
8493 return self .get ('iat' )
8594
8695 @property
87- def sub (self ):
96+ def sub (self ) -> str :
8897 """Returns the sub (subject) of the token, which is the phone number."""
8998 return self .get ('sub' )
9099
@@ -94,19 +103,11 @@ def claims(self):
94103 return self
95104
96105
97- class FpnvClient :
98- """The client for the Firebase Phone Number Verification service ."""
106+ class _FpnvService :
107+ """Service class that implements Firebase Phone Number Verification functionality ."""
99108 _project_id = None
100109
101110 def __init__ (self , app ):
102- """Initializes the FpnvClient.
103-
104- Args:
105- app: A firebase_admin.App instance.
106-
107- Raises:
108- ValueError: If the app is invalid or lacks a project ID.
109- """
110111 self ._project_id = app .project_id
111112
112113 if not self ._project_id :
@@ -154,30 +155,32 @@ def verify(self, token) -> Dict[str, Any]:
154155 try :
155156 self ._validate_headers (jwt .get_unverified_header (token ))
156157 signing_key = self ._jwks_client .get_signing_key_from_jwt (token )
157- claims = self ._validate_payload (token , signing_key .key )
158+ claims = self ._decode_and_verify (token , signing_key .key )
158159 except (InvalidTokenError , DecodeError , PyJWKClientError ) as exception :
159- raise ValueError (
160- f'Verifying FPNV token failed. Error: { exception } '
160+ raise InvalidFpnvTokenError (
161+ 'Verifying FPNV token failed.' ,
162+ cause = exception ,
163+ http_response = getattr (exception , 'http_response' , None )
161164 ) from exception
162165
163166 return claims
164167
165168 def _validate_headers (self , headers : Any ) -> None :
166169 """Validates the headers."""
167170 if headers .get ('kid' ) is None :
168- raise ValueError ("FPNV has no 'kid' claim." )
171+ raise InvalidFpnvTokenError ("FPNV has no 'kid' claim." )
169172
170173 if headers .get ('typ' ) != 'JWT' :
171- raise ValueError ("The provided FPNV token has an incorrect type header" )
174+ raise InvalidFpnvTokenError ("The provided FPNV token has an incorrect type header" )
172175
173176 algorithm = headers .get ('alg' )
174177 if algorithm != _ALGORITHM_ES256 :
175- raise ValueError (
178+ raise InvalidFpnvTokenError (
176179 'The provided FPNV token has an incorrect alg header. '
177180 f'Expected { _ALGORITHM_ES256 } but got { algorithm } .'
178181 )
179182
180- def _validate_payload (self , token : str , signing_key : str ) -> Dict [str , Any ]:
183+ def _decode_and_verify (self , token , signing_key ) -> Dict [str , Any ]:
181184 """Decodes and verifies the token."""
182185 expected_issuer = f'{ _FPNV_ISSUER } { self ._project_id } '
183186 try :
@@ -189,25 +192,25 @@ def _validate_payload(self, token: str, signing_key: str) -> Dict[str, Any]:
189192 issuer = expected_issuer
190193 )
191194 except InvalidSignatureError as exception :
192- raise ValueError (
195+ raise InvalidFpnvTokenError (
193196 'The provided FPNV token has an invalid signature.'
194197 ) from exception
195198 except InvalidAudienceError as exception :
196- raise ValueError (
199+ raise InvalidFpnvTokenError (
197200 'The provided FPNV token has an incorrect "aud" (audience) claim. '
198201 f'Expected payload to include { expected_issuer } .'
199202 ) from exception
200203 except InvalidIssuerError as exception :
201- raise ValueError (
204+ raise InvalidFpnvTokenError (
202205 'The provided FPNV token has an incorrect "iss" (issuer) claim. '
203206 f'Expected claim to include { expected_issuer } '
204207 ) from exception
205208 except ExpiredSignatureError as exception :
206- raise ValueError (
209+ raise ExpiredFpnvTokenError (
207210 'The provided FPNV token has expired.'
208211 ) from exception
209212 except InvalidTokenError as exception :
210- raise ValueError (
213+ raise InvalidFpnvTokenError (
211214 f'Decoding FPNV token failed. Error: { exception } '
212215 ) from exception
213216
@@ -229,3 +232,16 @@ def check_string(cls, label: str, value: Any):
229232 """Checks if the given value is a string."""
230233 if not isinstance (value , str ) or not value :
231234 raise ValueError (f'{ label } must be a non-empty string.' )
235+
236+ # Firebase Phone Number Verification (FPNV) Errors
237+ class InvalidFpnvTokenError (exceptions .InvalidArgumentError ):
238+ """Raised when an FPNV token is invalid."""
239+
240+ def __init__ (self , message , cause = None , http_response = None ):
241+ exceptions .InvalidArgumentError .__init__ (self , message , cause , http_response )
242+
243+ class ExpiredFpnvTokenError (InvalidFpnvTokenError ):
244+ """Raised when an FPNV token is expired."""
245+
246+ def __init__ (self , message , cause = None , http_response = None ):
247+ InvalidFpnvTokenError .__init__ (self , message , cause , http_response )
0 commit comments