Skip to content

Commit 61e10e5

Browse files
committed
chore: resolve lint and add more tests
1 parent b724099 commit 61e10e5

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

firebase_admin/fpnv.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class FpnvToken(dict):
5454
"""
5555

5656
def __init__(self, claims):
57-
super(FpnvToken, self).__init__(claims)
57+
super().__init__(claims)
5858

5959
@property
6060
def phone_number(self):
@@ -149,6 +149,7 @@ def __init__(self, project_id):
149149
self._jwks_client = PyJWKClient(_FPNV_JWKS_URL, lifespan=21600)
150150

151151
def verify(self, token) -> Dict[str, Any]:
152+
"""Verifies the given FPNV token."""
152153
_Validators.check_string("FPNV check token", token)
153154
try:
154155
self._validate_headers(jwt.get_unverified_header(token))
@@ -162,6 +163,7 @@ def verify(self, token) -> Dict[str, Any]:
162163
return claims
163164

164165
def _validate_headers(self, headers: Any) -> None:
166+
"""Validates the headers."""
165167
if headers.get('kid') is None:
166168
raise ValueError("FPNV has no 'kid' claim.")
167169

tests/test_fpnv.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414

1515
"""Test cases for the firebase_admin.fpnv module."""
1616

17+
import base64
18+
import time
1719
from unittest import mock
20+
from unittest.mock import patch
1821

1922
import jwt
2023
import pytest
24+
from cryptography.hazmat.primitives.asymmetric import ec
2125

2226
import firebase_admin
2327
from firebase_admin import fpnv
@@ -29,10 +33,13 @@
2933
_ISSUER = f'https://fpnv.googleapis.com/projects/{_PROJECT_ID}'
3034
_PHONE_NUMBER = '+1234567890'
3135
_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'
3239

3340
_MOCK_PAYLOAD = {
3441
'iss': _ISSUER,
35-
'sub': '+1234567890',
42+
'sub': _PHONE_NUMBER,
3643
'aud': [_ISSUER],
3744
'exp': _EXP_TIMESTAMP,
3845
'iat': _EXP_TIMESTAMP - 3600,
@@ -102,6 +109,61 @@ def test_client_explicit_app(self):
102109

103110
class TestVerifyToken(TestCommon):
104111

112+
def test_verify_token_with_real_crypto(self):
113+
"""Verifies a token signed with a real ES256 key pair.
114+
115+
Mocking only the JWKS endpoint.
116+
This ensures the cryptographic verification logic is functioning correctly.
117+
"""
118+
# Generate a real ES256 key pair
119+
private_key = ec.generate_private_key(ec.SECP256R1())
120+
public_key = private_key.public_key()
121+
122+
# Create the JWK representation of the public key (for the mock endpoint)
123+
# Note: Retrieving numbers from the key involves cryptography primitives
124+
public_numbers = public_key.public_numbers()
125+
126+
def to_b64url(b_data):
127+
return base64.urlsafe_b64encode(b_data).rstrip(b'=').decode('utf-8')
128+
129+
jwk = {
130+
"kty": "EC",
131+
"use": "sig",
132+
"alg": _ALGORITHM,
133+
"kid": _KEY_ID,
134+
"crv": "P-256",
135+
"x": to_b64url(public_numbers.x.to_bytes(32, 'big')),
136+
"y": to_b64url(public_numbers.y.to_bytes(32, 'big')),
137+
}
138+
now = int(time.time())
139+
payload = {
140+
'iss': _ISSUER,
141+
'aud': [_ISSUER],
142+
'iat': now,
143+
'exp': now + 3600,
144+
'sub': _PHONE_NUMBER
145+
}
146+
147+
# Sign using the private key object directly (PyJWT supports this)
148+
token = jwt.encode(
149+
payload,
150+
private_key,
151+
algorithm=_ALGORITHM,
152+
headers={'alg': _ALGORITHM, 'typ': _TYPE, 'kid': _KEY_ID},
153+
)
154+
155+
# Mock PyJWKClient fetch_data
156+
with patch('jwt.PyJWKClient.fetch_data') as mock_fetch:
157+
mock_fetch.return_value = {'keys': [jwk]}
158+
159+
app = firebase_admin.get_app()
160+
client = fpnv.client(app)
161+
decoded_token = client.verify_token(token)
162+
163+
assert decoded_token['sub'] == _PHONE_NUMBER
164+
assert _ISSUER in decoded_token['aud']
165+
assert decoded_token.phone_number == decoded_token['sub']
166+
105167
@mock.patch('jwt.PyJWKClient')
106168
@mock.patch('jwt.decode')
107169
@mock.patch('jwt.get_unverified_header')

0 commit comments

Comments
 (0)