Skip to content

Commit e25c7de

Browse files
committed
fix: Modified api usage to match approved specs
1 parent 1dee2e5 commit e25c7de

File tree

2 files changed

+114
-120
lines changed

2 files changed

+114
-120
lines changed

firebase_admin/fpnv.py

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,41 @@
1717
This module contains functions for verifying JWTs related to the Firebase
1818
Phone Number Verification (FPNV) service.
1919
"""
20-
from typing import Any, Dict
20+
from __future__ import annotations
21+
from typing import Any, Dict, Optional
2122

2223
import 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

4957
class 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

Comments
 (0)