Skip to content

Commit b779263

Browse files
committed
Accomodate auth emulator behaviour in tests.
Where possible, tests are modified to account for the current behaviour in emulator mode (e.g., invalid or expired tokens or cookies still work). In one case, signer verification didn't account for padding being stripped in JWT tokens, and was fixed to do so.
1 parent 6d09431 commit b779263

File tree

2 files changed

+124
-24
lines changed

2 files changed

+124
-24
lines changed

firebase_admin/_auth_client.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def verify_id_token(self, id_token, check_revoked=False):
126126
raise_auth_utils.TenantIdMismatchError(
127127
'Invalid tenant ID: {0}'.format(token_tenant_id))
128128

129-
ifnotself.emulatedandcheck_revoked:
129+
ifcheck_revoked:
130130
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
131131
returnverified_claims
132132

tests/test_token_gen.py

+123-23
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,19 @@ def _merge_jwt_claims(defaults, overrides):
7676

7777
defverify_custom_token(custom_token, expected_claims, tenant_id=None):
7878
assertisinstance(custom_token, bytes)
79-
token=google.oauth2.id_token.verify_token(
80-
custom_token,
81-
testutils.MockRequest(200, MOCK_PUBLIC_CERTS),
82-
_token_gen.FIREBASE_AUDIENCE)
79+
if_is_emulated():
80+
token=jwt.decode(custom_token, verify=False)
81+
else:
82+
token=google.oauth2.id_token.verify_token(
83+
custom_token,
84+
testutils.MockRequest(200, MOCK_PUBLIC_CERTS),
85+
_token_gen.FIREBASE_AUDIENCE)
8386
asserttoken['uid'] ==MOCK_UID
84-
asserttoken['iss'] ==MOCK_SERVICE_ACCOUNT_EMAIL
85-
asserttoken['sub'] ==MOCK_SERVICE_ACCOUNT_EMAIL
87+
expected_email=MOCK_SERVICE_ACCOUNT_EMAIL
88+
if_is_emulated():
89+
expected_email=_token_gen.AUTH_EMULATOR_EMAIL
90+
asserttoken['iss'] ==expected_email
91+
asserttoken['sub'] ==expected_email
8692
iftenant_idisNone:
8793
assert'tenant_id'notintoken
8894
else:
@@ -141,7 +147,15 @@ def _overwrite_iam_request(app, request):
141147
client=auth._get_client(app)
142148
client._token_generator.request=request
143149

144-
@pytest.fixture(scope='module', params=[{'emulated': False}, {'emulated': True}])
150+
151+
def_is_emulated():
152+
emulator_host=os.getenv(EMULATOR_HOST_ENV_VAR, '')
153+
returnemulator_hostand'//'notinemulator_host
154+
155+
156+
# These fixtures are set to the function scope as the emulator environment variable bleeds over when
157+
# in module scope.
158+
@pytest.fixture(scope='function', params=[{'emulated': False}, {'emulated': True}])
145159
defauth_app(request):
146160
"""Returns an App initialized with a mock service account credential.
147161
@@ -157,7 +171,7 @@ def auth_app(request):
157171
firebase_admin.delete_app(app)
158172
monkeypatch.undo()
159173

160-
@pytest.fixture(scope='module', params=[{'emulated': False}, {'emulated': True}])
174+
@pytest.fixture(scope='function', params=[{'emulated': False}, {'emulated': True}])
161175
defuser_mgt_app(request):
162176
monkeypatch=testutils.new_monkeypatch()
163177
ifrequest.param['emulated']:
@@ -230,20 +244,30 @@ def test_invalid_params(self, auth_app, values):
230244
auth.create_custom_token(user, claims, app=auth_app)
231245

232246
deftest_noncert_credential(self, user_mgt_app):
247+
if_is_emulated():
248+
# Should work fine with the emulator, so do a condensed version of
249+
# test_sign_with_iam below.
250+
custom_token=auth.create_custom_token(MOCK_UID, app=user_mgt_app).decode()
251+
self._verify_signer(custom_token, _token_gen.AUTH_EMULATOR_EMAIL)
252+
return
233253
withpytest.raises(ValueError):
234254
auth.create_custom_token(MOCK_UID, app=user_mgt_app)
235255

236256
deftest_sign_with_iam(self):
237-
options= {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
257+
signer=_token_gen.AUTH_EMULATOR_EMAILif_is_emulated() else'test-service-account'
258+
options= {'serviceAccountId': signer, 'projectId': 'mock-project-id'}
238259
app=firebase_admin.initialize_app(
239260
testutils.MockCredential(), name='iam-signer-app', options=options)
240261
try:
241262
signature=base64.b64encode(b'test').decode()
242263
iam_resp='{{"signature": "{0}"}}'.format(signature)
243264
_overwrite_iam_request(app, testutils.MockRequest(200, iam_resp))
244265
custom_token=auth.create_custom_token(MOCK_UID, app=app).decode()
245-
assertcustom_token.endswith('.'+signature.rstrip('='))
246-
self._verify_signer(custom_token, 'test-service-account')
266+
if_is_emulated():
267+
assertcustom_token.endswith('.')
268+
else:
269+
assertcustom_token.endswith('.'+signature.rstrip('='))
270+
self._verify_signer(custom_token, signer)
247271
finally:
248272
firebase_admin.delete_app(app)
249273

@@ -254,6 +278,12 @@ def test_sign_with_iam_error(self):
254278
try:
255279
iam_resp='{"error": {"code": 403, "message": "test error"}}'
256280
_overwrite_iam_request(app, testutils.MockRequest(403, iam_resp))
281+
if_is_emulated():
282+
# Should work fine with the emulator, so do a condensed version of
283+
# test_sign_with_iam above.
284+
custom_token=auth.create_custom_token(MOCK_UID, app=app).decode()
285+
self._verify_signer(custom_token, _token_gen.AUTH_EMULATOR_EMAIL)
286+
return
257287
withpytest.raises(auth.TokenSignError) asexcinfo:
258288
auth.create_custom_token(MOCK_UID, app=app)
259289
error=excinfo.value
@@ -264,7 +294,8 @@ def test_sign_with_iam_error(self):
264294
firebase_admin.delete_app(app)
265295

266296
deftest_sign_with_discovered_service_account(self):
267-
request=testutils.MockRequest(200, 'discovered-service-account')
297+
signer=_token_gen.AUTH_EMULATOR_EMAILif_is_emulated() else'discovered-service-account'
298+
request=testutils.MockRequest(200, signer)
268299
options= {'projectId': 'mock-project-id'}
269300
app=firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app',
270301
options=options)
@@ -279,10 +310,16 @@ def test_sign_with_discovered_service_account(self):
279310
request.response=testutils.MockResponse(
280311
200, '{{"signature": "{0}"}}'.format(signature))
281312
custom_token=auth.create_custom_token(MOCK_UID, app=app).decode()
282-
assertcustom_token.endswith('.'+signature.rstrip('='))
283-
self._verify_signer(custom_token, 'discovered-service-account')
284-
assertlen(request.log) ==2
285-
assertrequest.log[0][1]['headers'] == {'Metadata-Flavor': 'Google'}
313+
if_is_emulated():
314+
# No signature from the emulator
315+
assertcustom_token.endswith('.')
316+
# No requests will be made with the emulator
317+
assertlen(request.log) ==0
318+
else:
319+
assertcustom_token.endswith('.'+signature.rstrip('='))
320+
assertlen(request.log) ==2
321+
assertrequest.log[0][1]['headers'] == {'Metadata-Flavor': 'Google'}
322+
self._verify_signer(custom_token, signer)
286323
finally:
287324
firebase_admin.delete_app(app)
288325

@@ -293,6 +330,12 @@ def test_sign_with_discovery_failure(self):
293330
options=options)
294331
try:
295332
_overwrite_iam_request(app, request)
333+
if_is_emulated():
334+
# Should work fine with the emulator, so do a condensed version of
335+
# test_sign_with_iam above.
336+
custom_token=auth.create_custom_token(MOCK_UID, app=app).decode()
337+
self._verify_signer(custom_token, _token_gen.AUTH_EMULATOR_EMAIL)
338+
return
296339
withpytest.raises(ValueError) asexcinfo:
297340
auth.create_custom_token(MOCK_UID, app=app)
298341
assertstr(excinfo.value).startswith('Failed to determine service account: test error')
@@ -304,6 +347,14 @@ def test_sign_with_discovery_failure(self):
304347
def_verify_signer(self, token, signer):
305348
segments=token.split('.')
306349
assertlen(segments) ==3
350+
# See https://github.com/googleapis/google-auth-library-python/pull/324
351+
# and also RFC 7515 which is the basis of that PR:
352+
# JWT segments don't have padding and base64 decoding might fail.
353+
#
354+
# Workaround from https://stackoverflow.com/a/9807138/2072269
355+
missing_padding=len(segments[1]) %4
356+
ifmissing_padding:
357+
segments[1] +='='* (4-missing_padding)
307358
body=json.loads(base64.b64decode(segments[1]).decode())
308359
assertbody['iss'] ==signer
309360
assertbody['sub'] ==signer
@@ -406,6 +457,12 @@ class TestVerifyIdToken:
406457
'BadFormatToken': 'foobar'
407458
}
408459

460+
tokens_not_invalid_in_emulator= [
461+
'WrongKid',
462+
'FutureToken',
463+
'ExpiredToken'
464+
]
465+
409466
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
410467
deftest_valid_token(self, user_mgt_app, id_token):
411468
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
@@ -458,17 +515,31 @@ def test_invalid_arg(self, user_mgt_app, id_token):
458515
auth.verify_id_token(id_token, app=user_mgt_app)
459516
assert'Illegal ID token provided'instr(excinfo.value)
460517

461-
@pytest.mark.parametrize('id_token', invalid_tokens.values(), ids=list(invalid_tokens))
462-
deftest_invalid_token(self, user_mgt_app, id_token):
518+
@pytest.mark.parametrize('id_token_key', list(invalid_tokens))
519+
deftest_invalid_token(self, user_mgt_app, id_token_key):
520+
id_token=self.invalid_tokens[id_token_key]
521+
if_is_emulated() andid_token_keyinself.tokens_not_invalid_in_emulator:
522+
# These tokens won't be invalid when using the emulator, check them like valid tokens.
523+
claims=auth.verify_id_token(id_token, app=user_mgt_app)
524+
assertclaims['admin'] isTrue
525+
assertclaims['uid'] ==claims['sub']
526+
return
463527
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
464528
withpytest.raises(auth.InvalidIdTokenError) asexcinfo:
465529
auth.verify_id_token(id_token, app=user_mgt_app)
466530
assertisinstance(excinfo.value, exceptions.InvalidArgumentError)
467531
assertexcinfo.value.http_responseisNone
468532

469533
deftest_expired_token(self, user_mgt_app):
534+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
470535
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
471536
id_token=self.invalid_tokens['ExpiredToken']
537+
if_is_emulated():
538+
# This token won't be invalid when using the emulator, check it like a valid token.
539+
claims=auth.verify_id_token(id_token, app=user_mgt_app)
540+
assertclaims['admin'] isTrue
541+
assertclaims['uid'] ==claims['sub']
542+
return
472543
withpytest.raises(auth.ExpiredIdTokenError) asexcinfo:
473544
auth.verify_id_token(id_token, app=user_mgt_app)
474545
assertisinstance(excinfo.value, auth.InvalidIdTokenError)
@@ -506,6 +577,10 @@ def test_custom_token(self, auth_app):
506577

507578
deftest_certificate_request_failure(self, user_mgt_app):
508579
_overwrite_cert_request(user_mgt_app, testutils.MockRequest(404, 'not found'))
580+
if_is_emulated():
581+
# Shouldn't fetch certificates in emulator mode.
582+
auth.verify_id_token(TEST_ID_TOKEN, app=user_mgt_app)
583+
return
509584
withpytest.raises(auth.CertificateFetchError) asexcinfo:
510585
auth.verify_id_token(TEST_ID_TOKEN, app=user_mgt_app)
511586
assert'Could not fetch certificates'instr(excinfo.value)
@@ -540,6 +615,12 @@ class TestVerifySessionCookie:
540615
'IDToken': TEST_ID_TOKEN,
541616
}
542617

618+
cookies_not_invalid_in_emulator= [
619+
'WrongKid',
620+
'FutureCookie',
621+
'ExpiredCookie'
622+
]
623+
543624
@pytest.mark.parametrize('cookie', valid_cookies.values(), ids=list(valid_cookies))
544625
deftest_valid_cookie(self, user_mgt_app, cookie):
545626
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
@@ -578,8 +659,15 @@ def test_invalid_args(self, user_mgt_app, cookie):
578659
auth.verify_session_cookie(cookie, app=user_mgt_app)
579660
assert'Illegal session cookie provided'instr(excinfo.value)
580661

581-
@pytest.mark.parametrize('cookie', invalid_cookies.values(), ids=list(invalid_cookies))
582-
deftest_invalid_cookie(self, user_mgt_app, cookie):
662+
@pytest.mark.parametrize('cookie_key', list(invalid_cookies))
663+
deftest_invalid_cookie(self, user_mgt_app, cookie_key):
664+
cookie=self.invalid_cookies[cookie_key]
665+
if_is_emulated() andcookie_keyinself.cookies_not_invalid_in_emulator:
666+
# These cookies won't be invalid when using the emulator, check them like valid cookies.
667+
claims=auth.verify_session_cookie(cookie, app=user_mgt_app)
668+
assertclaims['admin'] isTrue
669+
assertclaims['uid'] ==claims['sub']
670+
return
583671
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
584672
withpytest.raises(auth.InvalidSessionCookieError) asexcinfo:
585673
auth.verify_session_cookie(cookie, app=user_mgt_app)
@@ -589,6 +677,12 @@ def test_invalid_cookie(self, user_mgt_app, cookie):
589677
deftest_expired_cookie(self, user_mgt_app):
590678
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
591679
cookie=self.invalid_cookies['ExpiredCookie']
680+
if_is_emulated():
681+
# This cookie won't be invalid when using the emulator, check it like a valid cookie.
682+
claims=auth.verify_session_cookie(cookie, app=user_mgt_app)
683+
assertclaims['admin'] isTrue
684+
assertclaims['uid'] ==claims['sub']
685+
return
592686
withpytest.raises(auth.ExpiredSessionCookieError) asexcinfo:
593687
auth.verify_session_cookie(cookie, app=user_mgt_app)
594688
assertisinstance(excinfo.value, auth.InvalidSessionCookieError)
@@ -621,6 +715,10 @@ def test_custom_token(self, auth_app):
621715

622716
deftest_certificate_request_failure(self, user_mgt_app):
623717
_overwrite_cert_request(user_mgt_app, testutils.MockRequest(404, 'not found'))
718+
if_is_emulated():
719+
# Shouldn't fetch certificates in emulator mode.
720+
auth.verify_session_cookie(TEST_SESSION_COOKIE, app=user_mgt_app)
721+
return
624722
withpytest.raises(auth.CertificateFetchError) asexcinfo:
625723
auth.verify_session_cookie(TEST_SESSION_COOKIE, app=user_mgt_app)
626724
assert'Could not fetch certificates'instr(excinfo.value)
@@ -637,9 +735,11 @@ def test_certificate_caching(self, user_mgt_app, httpserver):
637735
verifier.cookie_verifier.cert_url=httpserver.url
638736
verifier.id_token_verifier.cert_url=httpserver.url
639737
verifier.verify_session_cookie(TEST_SESSION_COOKIE)
640-
assertlen(httpserver.requests) ==1
738+
# No requests should be made in emulated mode
739+
request_count=0if_is_emulated() else1
740+
assertlen(httpserver.requests) ==request_count
641741
# Subsequent requests should not fetch certs from the server
642742
verifier.verify_session_cookie(TEST_SESSION_COOKIE)
643-
assertlen(httpserver.requests) ==1
743+
assertlen(httpserver.requests) ==request_count
644744
verifier.verify_id_token(TEST_ID_TOKEN)
645-
assertlen(httpserver.requests) ==1
745+
assertlen(httpserver.requests) ==request_count

0 commit comments

Comments
 (0)
close