firebase-admin 7.3.0__tar.gz → 7.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/PKG-INFO +1 -1
  2. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/__about__.py +1 -1
  3. firebase_admin-7.4.0/firebase_admin/phone_number_verification.py +250 -0
  4. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin.egg-info/PKG-INFO +1 -1
  5. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin.egg-info/SOURCES.txt +2 -0
  6. firebase_admin-7.4.0/tests/test_phone_number_verification.py +311 -0
  7. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/LICENSE +0 -0
  8. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/README.md +0 -0
  9. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/__init__.py +0 -0
  10. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_auth_client.py +0 -0
  11. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_auth_providers.py +0 -0
  12. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_auth_utils.py +0 -0
  13. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_http_client.py +0 -0
  14. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_messaging_encoder.py +0 -0
  15. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_messaging_utils.py +0 -0
  16. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_retry.py +0 -0
  17. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_rfc3339.py +0 -0
  18. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_sseclient.py +0 -0
  19. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_token_gen.py +0 -0
  20. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_user_identifier.py +0 -0
  21. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_user_import.py +0 -0
  22. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_user_mgt.py +0 -0
  23. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/_utils.py +0 -0
  24. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/app_check.py +0 -0
  25. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/auth.py +0 -0
  26. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/credentials.py +0 -0
  27. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/db.py +0 -0
  28. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/exceptions.py +0 -0
  29. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/firestore.py +0 -0
  30. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/firestore_async.py +0 -0
  31. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/functions.py +0 -0
  32. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/instance_id.py +0 -0
  33. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/messaging.py +0 -0
  34. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/ml.py +0 -0
  35. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/project_management.py +0 -0
  36. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/remote_config.py +0 -0
  37. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/storage.py +0 -0
  38. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin/tenant_mgt.py +0 -0
  39. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin.egg-info/dependency_links.txt +0 -0
  40. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin.egg-info/requires.txt +0 -0
  41. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/firebase_admin.egg-info/top_level.txt +0 -0
  42. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/setup.cfg +0 -0
  43. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/setup.py +0 -0
  44. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_app.py +0 -0
  45. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_app_check.py +0 -0
  46. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_auth_providers.py +0 -0
  47. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_credentials.py +0 -0
  48. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_db.py +0 -0
  49. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_exceptions.py +0 -0
  50. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_firestore.py +0 -0
  51. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_firestore_async.py +0 -0
  52. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_functions.py +0 -0
  53. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_http_client.py +0 -0
  54. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_instance_id.py +0 -0
  55. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_messaging.py +0 -0
  56. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_ml.py +0 -0
  57. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_project_management.py +0 -0
  58. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_remote_config.py +0 -0
  59. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_retry.py +0 -0
  60. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_rfc3339.py +0 -0
  61. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_sseclient.py +0 -0
  62. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_storage.py +0 -0
  63. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_tenant_mgt.py +0 -0
  64. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_token_gen.py +0 -0
  65. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/test_user_mgt.py +0 -0
  66. {firebase_admin-7.3.0 → firebase_admin-7.4.0}/tests/testutils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firebase_admin
3
- Version: 7.3.0
3
+ Version: 7.4.0
4
4
  Summary: Firebase Admin Python SDK
5
5
  Home-page: https://firebase.google.com/docs/admin/setup/
6
6
  Author: Firebase
@@ -14,7 +14,7 @@
14
14
 
15
15
  """About information (version, etc) for Firebase Admin SDK."""
16
16
 
17
- __version__ = '7.3.0'
17
+ __version__ = '7.4.0'
18
18
  __title__ = 'firebase_admin'
19
19
  __author__ = 'Firebase'
20
20
  __license__ = 'Apache License 2.0'
@@ -0,0 +1,250 @@
1
+ # Copyright 2026 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Firebase Phone Number Verification module.
16
+
17
+ This module contains functions for verifying JWTs related to the Firebase
18
+ Phone Number Verification service.
19
+ """
20
+ from __future__ import annotations
21
+ from typing import Any, Dict, Optional
22
+
23
+ import jwt
24
+ from jwt import (
25
+ PyJWKClient, InvalidSignatureError,
26
+ PyJWKClientError, InvalidAudienceError, InvalidIssuerError, ExpiredSignatureError
27
+ )
28
+
29
+ from firebase_admin import App, _utils, exceptions
30
+
31
+ _FPNV_ATTRIBUTE = '_phone_number_verification'
32
+ _FPNV_JWKS_URL = 'https://fpnv.googleapis.com/v1beta/jwks'
33
+ _FPNV_ISSUER = 'https://fpnv.googleapis.com/projects/'
34
+ _ALGORITHM_ES256 = 'ES256'
35
+
36
+
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) -> PhoneNumberVerificationToken:
41
+ """Verifies a Firebase Phone Number Verification token.
42
+
43
+ Args:
44
+ token: A string containing the Firebase Phone Number Verification JWT.
45
+ app: An App instance (optional).
46
+
47
+ Returns:
48
+ PhoneNumberVerificationToken: The verified token claims.
49
+
50
+ Raises:
51
+ ValueError: If the token is not a string or is empty.
52
+ InvalidTokenError: If the token is invalid or malformed.
53
+ ExpiredTokenError: If the token has expired.
54
+ """
55
+ return _get_fpnv_service(app).verify_token(token)
56
+
57
+
58
+ class PhoneNumberVerificationToken(dict):
59
+ """Represents a verified Firebase Phone Number Verification token.
60
+
61
+ This class behaves like a dictionary, allowing access to the decoded claims.
62
+ It also provides convenience properties for common claims.
63
+ """
64
+
65
+ def __init__(self, claims):
66
+ super().__init__(claims)
67
+ self['phone_number'] = claims.get('sub')
68
+
69
+ @property
70
+ def phone_number(self) -> str:
71
+ """Returns the phone number of the user.
72
+ This corresponds to the 'sub' claim in the JWT.
73
+ """
74
+ return self.get('sub')
75
+
76
+ @property
77
+ def issuer(self) -> str:
78
+ """Returns the issuer identifier for the issuer of the response."""
79
+ return self.get('iss')
80
+
81
+ @property
82
+ def audience(self) -> str:
83
+ """Returns the audience for which this token is intended."""
84
+ return self.get('aud')
85
+
86
+ @property
87
+ def exp(self) -> int:
88
+ """Returns the expiration time since the Unix epoch."""
89
+ return self.get('exp')
90
+
91
+ @property
92
+ def iat(self) -> int:
93
+ """Returns the issued-at time since the Unix epoch."""
94
+ return self.get('iat')
95
+
96
+ @property
97
+ def sub(self) -> str:
98
+ """Returns the sub (subject) of the token, which is the phone number."""
99
+ return self.get('sub')
100
+
101
+ @property
102
+ def claims(self):
103
+ """Returns the entire map of claims."""
104
+ return self
105
+
106
+
107
+ class _FpnvService:
108
+ """Service class that implements Firebase Phone Number Verification functionality."""
109
+ _project_id = None
110
+
111
+ def __init__(self, app):
112
+ self._project_id = app.project_id
113
+ if not self._project_id:
114
+ raise ValueError(
115
+ 'Project ID is required for Firebase Phone Number Verification. Please ensure the '
116
+ 'app is initialized with a credential that contains a project ID.'
117
+ )
118
+
119
+ self._verifier = _FpnvTokenVerifier(self._project_id)
120
+
121
+ def verify_token(self, token) -> PhoneNumberVerificationToken:
122
+ """Verifies a Firebase Phone Number Verification token.
123
+
124
+ Verifies the signature, expiration, and claims of the token.
125
+
126
+ Args:
127
+ token: A string containing the Firebase Phone Number Verification JWT.
128
+
129
+ Returns:
130
+ PhoneNumberVerificationToken: The verified token claims.
131
+
132
+ Raises:
133
+ ValueError: If the token is not a string or is empty.
134
+ InvalidTokenError: If the token is invalid or malformed.
135
+ ExpiredTokenError: If the token has expired.
136
+ """
137
+ return PhoneNumberVerificationToken(self._verifier.verify(token))
138
+
139
+
140
+ class _FpnvTokenVerifier:
141
+ """Internal class for verifying Firebase Phone Number Verification JWTs signed with ES256."""
142
+ _jwks_client = None
143
+ _project_id = None
144
+
145
+ def __init__(self, project_id):
146
+ self._project_id = project_id
147
+ self._jwks_client = PyJWKClient(_FPNV_JWKS_URL, lifespan=21600)
148
+
149
+ def verify(self, token) -> Dict[str, Any]:
150
+ """Verifies the given Firebase Phone Number Verification token."""
151
+ _Validators.check_string("Firebase Phone Number Verification check token", token)
152
+ try:
153
+ self._validate_headers(jwt.get_unverified_header(token))
154
+ signing_key = self._jwks_client.get_signing_key_from_jwt(token)
155
+ claims = self._decode_and_verify(token, signing_key.key)
156
+ except (jwt.InvalidTokenError, PyJWKClientError) as exception:
157
+ raise InvalidTokenError(
158
+ 'Verifying phone number verification token failed.',
159
+ cause=exception,
160
+ http_response=getattr(exception, 'http_response', None)
161
+ ) from exception
162
+
163
+ return claims
164
+
165
+ def _validate_headers(self, headers: Any) -> None:
166
+ """Validates the headers."""
167
+ if headers.get('kid') is None:
168
+ raise InvalidTokenError("Token has no 'kid' claim.")
169
+
170
+ if headers.get('typ') != 'JWT':
171
+ raise InvalidTokenError(
172
+ 'The provided token has an incorrect type header. ' \
173
+ f"Expected 'JWT' but got {headers.get('typ')!r}."
174
+ )
175
+
176
+ algorithm = headers.get('alg')
177
+ if algorithm != _ALGORITHM_ES256:
178
+ raise InvalidTokenError(
179
+ 'The provided token has an incorrect alg header. '
180
+ f'Expected {_ALGORITHM_ES256} but got {algorithm}.'
181
+ )
182
+
183
+ def _decode_and_verify(self, token, signing_key) -> Dict[str, Any]:
184
+ """Decodes and verifies the token."""
185
+ expected_issuer = f'{_FPNV_ISSUER}{self._project_id}'
186
+ try:
187
+ payload = jwt.decode(
188
+ token,
189
+ signing_key,
190
+ algorithms=[_ALGORITHM_ES256],
191
+ audience=expected_issuer,
192
+ issuer=expected_issuer
193
+ )
194
+ except InvalidSignatureError as exception:
195
+ raise InvalidTokenError(
196
+ 'The provided token has an invalid signature.'
197
+ ) from exception
198
+ except InvalidAudienceError as exception:
199
+ raise InvalidTokenError(
200
+ 'The provided token has an incorrect "aud" (audience) claim. '
201
+ f'Expected {expected_issuer}.'
202
+ ) from exception
203
+ except InvalidIssuerError as exception:
204
+ raise InvalidTokenError(
205
+ 'The provided token has an incorrect "iss" (issuer) claim. '
206
+ f'Expected {expected_issuer}.'
207
+ ) from exception
208
+ except ExpiredSignatureError as exception:
209
+ raise ExpiredTokenError(
210
+ 'The provided token has expired.'
211
+ ) from exception
212
+ except jwt.InvalidTokenError as exception:
213
+ raise InvalidTokenError(
214
+ f'Decoding token failed. Error: {exception}'
215
+ ) from exception
216
+
217
+ sub_claim = payload.get('sub')
218
+ if not isinstance(sub_claim, str) or not sub_claim:
219
+ raise InvalidTokenError(
220
+ 'The provided token has an incorrect "sub" (subject) claim. '
221
+ 'Expected a non-empty string.'
222
+ )
223
+
224
+ return payload
225
+
226
+
227
+ class _Validators:
228
+ """A collection of data validation utilities.
229
+
230
+ Methods provided in this class raise ``ValueErrors`` if any validations fail.
231
+ """
232
+
233
+ @classmethod
234
+ def check_string(cls, label: str, value: Any):
235
+ """Checks if the given value is a string."""
236
+ if not isinstance(value, str) or not value:
237
+ raise ValueError(f'{label} must be a non-empty string.')
238
+
239
+ # Firebase Phone Number Verification Errors
240
+ class InvalidTokenError(exceptions.InvalidArgumentError):
241
+ """Raised when a Firebase Phone Number Verification token is invalid."""
242
+
243
+ def __init__(self, message, cause=None, http_response=None):
244
+ exceptions.InvalidArgumentError.__init__(self, message, cause, http_response)
245
+
246
+ class ExpiredTokenError(InvalidTokenError):
247
+ """Raised when a Firebase Phone Number Verification token is expired."""
248
+
249
+ def __init__(self, message, cause=None, http_response=None):
250
+ InvalidTokenError.__init__(self, message, cause, http_response)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firebase_admin
3
- Version: 7.3.0
3
+ Version: 7.4.0
4
4
  Summary: Firebase Admin Python SDK
5
5
  Home-page: https://firebase.google.com/docs/admin/setup/
6
6
  Author: Firebase
@@ -29,6 +29,7 @@ firebase_admin/functions.py
29
29
  firebase_admin/instance_id.py
30
30
  firebase_admin/messaging.py
31
31
  firebase_admin/ml.py
32
+ firebase_admin/phone_number_verification.py
32
33
  firebase_admin/project_management.py
33
34
  firebase_admin/remote_config.py
34
35
  firebase_admin/storage.py
@@ -51,6 +52,7 @@ tests/test_http_client.py
51
52
  tests/test_instance_id.py
52
53
  tests/test_messaging.py
53
54
  tests/test_ml.py
55
+ tests/test_phone_number_verification.py
54
56
  tests/test_project_management.py
55
57
  tests/test_remote_config.py
56
58
  tests/test_retry.py
@@ -0,0 +1,311 @@
1
+ # Copyright 2026 Google Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Test cases for the firebase_admin.fpnv module."""
16
+
17
+ import base64
18
+ import time
19
+ from unittest import mock
20
+ from unittest.mock import patch
21
+
22
+ import jwt
23
+ import pytest
24
+ from cryptography.hazmat.primitives.asymmetric import ec
25
+
26
+ import firebase_admin
27
+ from firebase_admin import phone_number_verification as fpnv
28
+ from tests import testutils
29
+
30
+ # Mock Data
31
+ _PROJECT_ID = 'mock-project-id'
32
+ _EXP_TIMESTAMP = 2000000000
33
+ _ISSUER = f'https://fpnv.googleapis.com/projects/{_PROJECT_ID}'
34
+ _PHONE_NUMBER = '+1234567890'
35
+ _PUBLIC_KEY = 'test-public-key' # In real tests, use the corresponding public key
36
+ _ALGORITHM = 'ES256'
37
+ _KEY_ID = 'test-key-id'
38
+ _TYPE = 'JWT'
39
+
40
+ _MOCK_PAYLOAD = {
41
+ 'iss': _ISSUER,
42
+ 'sub': _PHONE_NUMBER,
43
+ 'aud': [_ISSUER],
44
+ 'exp': _EXP_TIMESTAMP,
45
+ 'iat': _EXP_TIMESTAMP - 3600,
46
+ "other": 'other'
47
+ }
48
+
49
+
50
+
51
+
52
+ class TestCommon:
53
+ @classmethod
54
+ def setup_class(cls):
55
+ cred = testutils.MockCredential()
56
+ firebase_admin.initialize_app(cred, {'projectId': _PROJECT_ID})
57
+
58
+ @classmethod
59
+ def teardown_class(cls):
60
+ testutils.cleanup_apps()
61
+
62
+
63
+ class TestFpnvToken:
64
+ def test_properties(self):
65
+ token = fpnv.PhoneNumberVerificationToken(_MOCK_PAYLOAD)
66
+
67
+ assert token.phone_number == _PHONE_NUMBER
68
+ assert token.sub == _PHONE_NUMBER
69
+ assert token.issuer == _ISSUER
70
+ assert token.audience == [_ISSUER]
71
+ expected_claims = _MOCK_PAYLOAD.copy()
72
+ expected_claims['phone_number'] = _PHONE_NUMBER
73
+ assert token.claims == expected_claims
74
+ assert token['other'] == _MOCK_PAYLOAD['other']
75
+
76
+
77
+ class TestVerifyToken(TestCommon):
78
+
79
+ def test_no_project_id(self):
80
+ app = firebase_admin.initialize_app(testutils.MockCredential(), name='no_project_id')
81
+ app.credential.get_credential().project_id = None
82
+ with pytest.raises(
83
+ ValueError,
84
+ match='Project ID is required for Firebase Phone Number Verification'
85
+ ):
86
+ fpnv.verify_token('token', app=app)
87
+
88
+ def test_verify_token_with_real_crypto(self):
89
+ """Verifies a token signed with a real ES256 key pair.
90
+
91
+ Mocking only the JWKS endpoint.
92
+ This ensures the cryptographic verification logic is functioning correctly.
93
+ """
94
+ # Generate a real ES256 key pair
95
+ private_key = ec.generate_private_key(ec.SECP256R1())
96
+ public_key = private_key.public_key()
97
+
98
+ # Create the JWK representation of the public key (for the mock endpoint)
99
+ # Note: Retrieving numbers from the key involves cryptography primitives
100
+ public_numbers = public_key.public_numbers()
101
+
102
+ def to_b64url(b_data):
103
+ return base64.urlsafe_b64encode(b_data).rstrip(b'=').decode('utf-8')
104
+
105
+ jwk = {
106
+ "kty": "EC",
107
+ "use": "sig",
108
+ "alg": _ALGORITHM,
109
+ "kid": _KEY_ID,
110
+ "crv": "P-256",
111
+ "x": to_b64url(public_numbers.x.to_bytes(32, 'big')),
112
+ "y": to_b64url(public_numbers.y.to_bytes(32, 'big')),
113
+ }
114
+ now = int(time.time())
115
+ payload = {
116
+ 'iss': _ISSUER,
117
+ 'aud': [_ISSUER],
118
+ 'iat': now,
119
+ 'exp': now + 3600,
120
+ 'sub': _PHONE_NUMBER
121
+ }
122
+
123
+ # Sign using the private key object directly (PyJWT supports this)
124
+ token = jwt.encode(
125
+ payload,
126
+ private_key,
127
+ algorithm=_ALGORITHM,
128
+ headers={'alg': _ALGORITHM, 'typ': _TYPE, 'kid': _KEY_ID},
129
+ )
130
+
131
+ # Mock PyJWKClient fetch_data
132
+ with patch('jwt.PyJWKClient.fetch_data') as mock_fetch:
133
+ mock_fetch.return_value = {'keys': [jwk]}
134
+
135
+ app = firebase_admin.get_app()
136
+ decoded_token = fpnv.verify_token(token, app)
137
+
138
+ assert decoded_token['sub'] == _PHONE_NUMBER
139
+ assert _ISSUER in decoded_token['aud']
140
+ assert decoded_token.phone_number == decoded_token['sub']
141
+ # Test convenience dictionary lookup
142
+ assert decoded_token['phone_number'] == _PHONE_NUMBER
143
+
144
+ def test_verify_token_module_level_delegation(self):
145
+ """Verifies module-level verify_token delegates correctly."""
146
+ with patch(
147
+ 'firebase_admin.phone_number_verification._FpnvService.verify_token'
148
+ ) as mock_verify:
149
+ mock_verify.return_value = 'mock-result'
150
+ res = fpnv.verify_token('some-token')
151
+ assert res == 'mock-result'
152
+ mock_verify.assert_called_once_with('some-token')
153
+
154
+ @mock.patch('jwt.PyJWKClient')
155
+ @mock.patch('jwt.decode')
156
+ @mock.patch('jwt.get_unverified_header')
157
+ def test_verify_token_success(self, mock_header, mock_decode, mock_jwks_cls):
158
+ token_str = 'valid.token.string'
159
+ # Mock Header
160
+ mock_header.return_value = {'kid': 'key1', 'typ': 'JWT', 'alg': 'ES256'}
161
+
162
+ # Mock Signing Key
163
+ mock_jwks_instance = mock_jwks_cls.return_value
164
+ mock_signing_key = mock.Mock()
165
+ mock_signing_key.key = _PUBLIC_KEY
166
+ mock_jwks_instance.get_signing_key_from_jwt.return_value = mock_signing_key
167
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
168
+ service._verifier._jwks_client = mock_jwks_instance
169
+
170
+ mock_decode.return_value = _MOCK_PAYLOAD
171
+
172
+ # Execute
173
+ token = fpnv.verify_token(token_str)
174
+
175
+ # Verify
176
+ assert isinstance(token, fpnv.PhoneNumberVerificationToken)
177
+ assert token.phone_number == _PHONE_NUMBER
178
+
179
+ mock_header.assert_called_with(token_str)
180
+ mock_jwks_instance.get_signing_key_from_jwt.assert_called_with(token_str)
181
+ mock_decode.assert_called_with(
182
+ token_str,
183
+ _PUBLIC_KEY,
184
+ algorithms=['ES256'],
185
+ audience=_ISSUER,
186
+ issuer=_ISSUER
187
+ )
188
+
189
+ @mock.patch('jwt.get_unverified_header')
190
+ def test_verify_token_no_name(self, mock_header):
191
+ app = firebase_admin.get_app()
192
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
193
+ with pytest.raises(ValueError, match="must be a non-empty string"):
194
+ fpnv.verify_token('', app=app)
195
+
196
+ @mock.patch('jwt.get_unverified_header')
197
+ def test_verify_token_no_kid(self, mock_header):
198
+ app = firebase_admin.get_app()
199
+ mock_header.return_value = {'typ': 'JWT', 'alg': 'ES256'} # Missing kid
200
+ with pytest.raises(fpnv.InvalidTokenError, match="Token has no 'kid' claim."):
201
+ fpnv.verify_token('token', app=app)
202
+
203
+ @mock.patch('jwt.get_unverified_header')
204
+ def test_verify_token_wrong_alg(self, mock_header):
205
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'RS256'} # Wrong alg
206
+ with pytest.raises(fpnv.InvalidTokenError, match="incorrect alg"):
207
+ fpnv.verify_token('token')
208
+
209
+ @mock.patch('jwt.get_unverified_header')
210
+ def test_verify_token_wrong_typ(self, mock_header):
211
+ mock_header.return_value = {'kid': 'k', 'typ': 'WRONG', 'alg': 'ES256'} # wrong typ
212
+ with pytest.raises(fpnv.InvalidTokenError, match="incorrect type header"):
213
+ fpnv.verify_token('token')
214
+
215
+ def test_verify_token_jwk_error(self):
216
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
217
+ jwks_client = service._verifier._jwks_client
218
+
219
+ # Mock the method on the existing instance
220
+ with mock.patch.object(jwks_client, 'get_signing_key_from_jwt') as mock_method:
221
+ mock_method.side_effect = jwt.PyJWKClientError("Key not found")
222
+
223
+ # Mock header is still needed if _get_signing_key calls it before the client
224
+ with mock.patch('jwt.get_unverified_header') as mock_header:
225
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
226
+
227
+ with pytest.raises(
228
+ fpnv.InvalidTokenError,
229
+ match="Verifying phone number verification token failed"
230
+ ):
231
+ fpnv.verify_token('token')
232
+
233
+ @mock.patch('jwt.PyJWKClient')
234
+ @mock.patch('jwt.decode')
235
+ @mock.patch('jwt.get_unverified_header')
236
+ def test_verify_token_expired(self, mock_header, mock_decode, mock_jwks_cls):
237
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
238
+ mock_jwks_instance = mock_jwks_cls.return_value
239
+ mock_jwks_instance.get_signing_key_from_jwt.return_value.key = _PUBLIC_KEY
240
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
241
+ service._verifier._jwks_client = mock_jwks_instance
242
+
243
+ # Simulate ExpiredSignatureError
244
+ mock_decode.side_effect = jwt.ExpiredSignatureError("Expired")
245
+
246
+ with pytest.raises(fpnv.ExpiredTokenError, match="token has expired"):
247
+ fpnv.verify_token('token')
248
+
249
+ @mock.patch('jwt.PyJWKClient')
250
+ @mock.patch('jwt.decode')
251
+ @mock.patch('jwt.get_unverified_header')
252
+ def test_verify_token_invalid_signature(self, mock_header, mock_decode, mock_jwks_cls):
253
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
254
+ mock_jwks_instance = mock_jwks_cls.return_value
255
+ mock_jwks_instance.get_signing_key_from_jwt.return_value.key = _PUBLIC_KEY
256
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
257
+ service._verifier._jwks_client = mock_jwks_instance
258
+
259
+ # Simulate InvalidSignatureError
260
+ mock_decode.side_effect = jwt.InvalidSignatureError("Wrong Signature")
261
+
262
+ with pytest.raises(fpnv.InvalidTokenError, match="invalid signature"):
263
+ fpnv.verify_token('token')
264
+
265
+ @mock.patch('jwt.PyJWKClient')
266
+ @mock.patch('jwt.decode')
267
+ @mock.patch('jwt.get_unverified_header')
268
+ def test_verify_token_invalid_audience(self, mock_header, mock_decode, mock_jwks_cls):
269
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
270
+ mock_jwks_instance = mock_jwks_cls.return_value
271
+ mock_jwks_instance.get_signing_key_from_jwt.return_value.key = _PUBLIC_KEY
272
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
273
+ service._verifier._jwks_client = mock_jwks_instance
274
+
275
+ # Simulate InvalidAudienceError
276
+ mock_decode.side_effect = jwt.InvalidAudienceError("Wrong Aud")
277
+
278
+ with pytest.raises(fpnv.InvalidTokenError, match="incorrect \"aud\""):
279
+ fpnv.verify_token('token')
280
+
281
+ @mock.patch('jwt.PyJWKClient')
282
+ @mock.patch('jwt.decode')
283
+ @mock.patch('jwt.get_unverified_header')
284
+ def test_verify_token_invalid_issuer(self, mock_header, mock_decode, mock_jwks_cls):
285
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
286
+ mock_jwks_instance = mock_jwks_cls.return_value
287
+ mock_jwks_instance.get_signing_key_from_jwt.return_value.key = _PUBLIC_KEY
288
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
289
+ service._verifier._jwks_client = mock_jwks_instance
290
+
291
+ # Simulate InvalidIssuerError
292
+ mock_decode.side_effect = jwt.InvalidIssuerError("Wrong Iss")
293
+
294
+ with pytest.raises(fpnv.InvalidTokenError, match="incorrect \"iss\""):
295
+ fpnv.verify_token('token')
296
+
297
+ @mock.patch('jwt.PyJWKClient')
298
+ @mock.patch('jwt.decode')
299
+ @mock.patch('jwt.get_unverified_header')
300
+ def test_verify_token_invalid_token(self, mock_header, mock_decode, mock_jwks_cls):
301
+ mock_header.return_value = {'kid': 'k', 'typ': 'JWT', 'alg': 'ES256'}
302
+ mock_jwks_instance = mock_jwks_cls.return_value
303
+ mock_jwks_instance.get_signing_key_from_jwt.return_value.key = _PUBLIC_KEY
304
+ service = fpnv._get_fpnv_service(firebase_admin.get_app())
305
+ service._verifier._jwks_client = mock_jwks_instance
306
+
307
+ # Simulate InvalidTokenError
308
+ mock_decode.side_effect = jwt.InvalidTokenError("Decoding FPNV token failed")
309
+
310
+ with pytest.raises(fpnv.InvalidTokenError, match="Decoding FPNV token failed"):
311
+ fpnv.verify_token('token')
File without changes
File without changes
File without changes
File without changes