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

Lines changed: 1 addition & 1 deletion
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-
if not self.emulated and check_revoked:
129+
if check_revoked:
130130
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
131131
return verified_claims
132132

tests/test_token_gen.py

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

7777
def verify_custom_token(custom_token, expected_claims, tenant_id=None):
7878
assert isinstance(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
assert token['uid'] == MOCK_UID
84-
assert token['iss'] == MOCK_SERVICE_ACCOUNT_EMAIL
85-
assert token['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+
assert token['iss'] == expected_email
91+
assert token['sub'] == expected_email
8692
if tenant_id is None:
8793
assert 'tenant_id' not in token
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+
return emulator_host and '//' not in emulator_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
def auth_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
def user_mgt_app(request):
162176
monkeypatch = testutils.new_monkeypatch()
163177
if request.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
def test_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
with pytest.raises(ValueError):
234254
auth.create_custom_token(MOCK_UID, app=user_mgt_app)
235255

236256
def test_sign_with_iam(self):
237-
options = {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
257+
signer = _token_gen.AUTH_EMULATOR_EMAIL if _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-
assert custom_token.endswith('.' + signature.rstrip('='))
246-
self._verify_signer(custom_token, 'test-service-account')
266+
if _is_emulated():
267+
assert custom_token.endswith('.')
268+
else:
269+
assert custom_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
with pytest.raises(auth.TokenSignError) as excinfo:
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
def test_sign_with_discovered_service_account(self):
267-
request = testutils.MockRequest(200, 'discovered-service-account')
297+
signer = _token_gen.AUTH_EMULATOR_EMAIL if _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-
assert custom_token.endswith('.' + signature.rstrip('='))
283-
self._verify_signer(custom_token, 'discovered-service-account')
284-
assert len(request.log) == 2
285-
assert request.log[0][1]['headers'] == {'Metadata-Flavor': 'Google'}
313+
if _is_emulated():
314+
# No signature from the emulator
315+
assert custom_token.endswith('.')
316+
# No requests will be made with the emulator
317+
assert len(request.log) == 0
318+
else:
319+
assert custom_token.endswith('.' + signature.rstrip('='))
320+
assert len(request.log) == 2
321+
assert request.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
with pytest.raises(ValueError) as excinfo:
297340
auth.create_custom_token(MOCK_UID, app=app)
298341
assert str(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
assert len(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+
if missing_padding:
357+
segments[1] += '=' * (4 - missing_padding)
307358
body = json.loads(base64.b64decode(segments[1]).decode())
308359
assert body['iss'] == signer
309360
assert body['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
def test_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' in str(excinfo.value)
460517

461-
@pytest.mark.parametrize('id_token', invalid_tokens.values(), ids=list(invalid_tokens))
462-
def test_invalid_token(self, user_mgt_app, id_token):
518+
@pytest.mark.parametrize('id_token_key', list(invalid_tokens))
519+
def test_invalid_token(self, user_mgt_app, id_token_key):
520+
id_token = self.invalid_tokens[id_token_key]
521+
if _is_emulated() and id_token_key in self.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+
assert claims['admin'] is True
525+
assert claims['uid'] == claims['sub']
526+
return
463527
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
464528
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
465529
auth.verify_id_token(id_token, app=user_mgt_app)
466530
assert isinstance(excinfo.value, exceptions.InvalidArgumentError)
467531
assert excinfo.value.http_response is None
468532

469533
def test_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+
assert claims['admin'] is True
541+
assert claims['uid'] == claims['sub']
542+
return
472543
with pytest.raises(auth.ExpiredIdTokenError) as excinfo:
473544
auth.verify_id_token(id_token, app=user_mgt_app)
474545
assert isinstance(excinfo.value, auth.InvalidIdTokenError)
@@ -506,6 +577,10 @@ def test_custom_token(self, auth_app):
506577

507578
def test_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
with pytest.raises(auth.CertificateFetchError) as excinfo:
510585
auth.verify_id_token(TEST_ID_TOKEN, app=user_mgt_app)
511586
assert 'Could not fetch certificates' in str(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
def test_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' in str(excinfo.value)
580661

581-
@pytest.mark.parametrize('cookie', invalid_cookies.values(), ids=list(invalid_cookies))
582-
def test_invalid_cookie(self, user_mgt_app, cookie):
662+
@pytest.mark.parametrize('cookie_key', list(invalid_cookies))
663+
def test_invalid_cookie(self, user_mgt_app, cookie_key):
664+
cookie = self.invalid_cookies[cookie_key]
665+
if _is_emulated() and cookie_key in self.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+
assert claims['admin'] is True
669+
assert claims['uid'] == claims['sub']
670+
return
583671
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
584672
with pytest.raises(auth.InvalidSessionCookieError) as excinfo:
585673
auth.verify_session_cookie(cookie, app=user_mgt_app)
@@ -589,6 +677,12 @@ def test_invalid_cookie(self, user_mgt_app, cookie):
589677
def test_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+
assert claims['admin'] is True
684+
assert claims['uid'] == claims['sub']
685+
return
592686
with pytest.raises(auth.ExpiredSessionCookieError) as excinfo:
593687
auth.verify_session_cookie(cookie, app=user_mgt_app)
594688
assert isinstance(excinfo.value, auth.InvalidSessionCookieError)
@@ -621,6 +715,10 @@ def test_custom_token(self, auth_app):
621715

622716
def test_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
with pytest.raises(auth.CertificateFetchError) as excinfo:
625723
auth.verify_session_cookie(TEST_SESSION_COOKIE, app=user_mgt_app)
626724
assert 'Could not fetch certificates' in str(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-
assert len(httpserver.requests) == 1
738+
# No requests should be made in emulated mode
739+
request_count = 0 if _is_emulated() else 1
740+
assert len(httpserver.requests) == request_count
641741
# Subsequent requests should not fetch certs from the server
642742
verifier.verify_session_cookie(TEST_SESSION_COOKIE)
643-
assert len(httpserver.requests) == 1
743+
assert len(httpserver.requests) == request_count
644744
verifier.verify_id_token(TEST_ID_TOKEN)
645-
assert len(httpserver.requests) == 1
745+
assert len(httpserver.requests) == request_count

0 commit comments

Comments
 (0)