@@ -76,13 +76,19 @@ def _merge_jwt_claims(defaults, overrides):
76
76
77
77
def verify_custom_token (custom_token , expected_claims , tenant_id = None ):
78
78
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 )
83
86
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
86
92
if tenant_id is None :
87
93
assert 'tenant_id' not in token
88
94
else :
@@ -141,7 +147,15 @@ def _overwrite_iam_request(app, request):
141
147
client = auth ._get_client (app )
142
148
client ._token_generator .request = request
143
149
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 }])
145
159
def auth_app (request ):
146
160
"""Returns an App initialized with a mock service account credential.
147
161
@@ -157,7 +171,7 @@ def auth_app(request):
157
171
firebase_admin .delete_app (app )
158
172
monkeypatch .undo ()
159
173
160
- @pytest .fixture (scope = 'module ' , params = [{'emulated' : False }, {'emulated' : True }])
174
+ @pytest .fixture (scope = 'function ' , params = [{'emulated' : False }, {'emulated' : True }])
161
175
def user_mgt_app (request ):
162
176
monkeypatch = testutils .new_monkeypatch ()
163
177
if request .param ['emulated' ]:
@@ -230,20 +244,30 @@ def test_invalid_params(self, auth_app, values):
230
244
auth .create_custom_token (user , claims , app = auth_app )
231
245
232
246
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
233
253
with pytest .raises (ValueError ):
234
254
auth .create_custom_token (MOCK_UID , app = user_mgt_app )
235
255
236
256
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' }
238
259
app = firebase_admin .initialize_app (
239
260
testutils .MockCredential (), name = 'iam-signer-app' , options = options )
240
261
try :
241
262
signature = base64 .b64encode (b'test' ).decode ()
242
263
iam_resp = '{{"signature": "{0}"}}' .format (signature )
243
264
_overwrite_iam_request (app , testutils .MockRequest (200 , iam_resp ))
244
265
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 )
247
271
finally :
248
272
firebase_admin .delete_app (app )
249
273
@@ -254,6 +278,12 @@ def test_sign_with_iam_error(self):
254
278
try :
255
279
iam_resp = '{"error": {"code": 403, "message": "test error"}}'
256
280
_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
257
287
with pytest .raises (auth .TokenSignError ) as excinfo :
258
288
auth .create_custom_token (MOCK_UID , app = app )
259
289
error = excinfo .value
@@ -264,7 +294,8 @@ def test_sign_with_iam_error(self):
264
294
firebase_admin .delete_app (app )
265
295
266
296
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 )
268
299
options = {'projectId' : 'mock-project-id' }
269
300
app = firebase_admin .initialize_app (testutils .MockCredential (), name = 'iam-signer-app' ,
270
301
options = options )
@@ -279,10 +310,16 @@ def test_sign_with_discovered_service_account(self):
279
310
request .response = testutils .MockResponse (
280
311
200 , '{{"signature": "{0}"}}' .format (signature ))
281
312
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 )
286
323
finally :
287
324
firebase_admin .delete_app (app )
288
325
@@ -293,6 +330,12 @@ def test_sign_with_discovery_failure(self):
293
330
options = options )
294
331
try :
295
332
_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
296
339
with pytest .raises (ValueError ) as excinfo :
297
340
auth .create_custom_token (MOCK_UID , app = app )
298
341
assert str (excinfo .value ).startswith ('Failed to determine service account: test error' )
@@ -304,6 +347,14 @@ def test_sign_with_discovery_failure(self):
304
347
def _verify_signer (self , token , signer ):
305
348
segments = token .split ('.' )
306
349
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 )
307
358
body = json .loads (base64 .b64decode (segments [1 ]).decode ())
308
359
assert body ['iss' ] == signer
309
360
assert body ['sub' ] == signer
@@ -406,6 +457,12 @@ class TestVerifyIdToken:
406
457
'BadFormatToken' : 'foobar'
407
458
}
408
459
460
+ tokens_not_invalid_in_emulator = [
461
+ 'WrongKid' ,
462
+ 'FutureToken' ,
463
+ 'ExpiredToken'
464
+ ]
465
+
409
466
@pytest .mark .parametrize ('id_token' , valid_tokens .values (), ids = list (valid_tokens ))
410
467
def test_valid_token (self , user_mgt_app , id_token ):
411
468
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
@@ -458,17 +515,31 @@ def test_invalid_arg(self, user_mgt_app, id_token):
458
515
auth .verify_id_token (id_token , app = user_mgt_app )
459
516
assert 'Illegal ID token provided' in str (excinfo .value )
460
517
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
463
527
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
464
528
with pytest .raises (auth .InvalidIdTokenError ) as excinfo :
465
529
auth .verify_id_token (id_token , app = user_mgt_app )
466
530
assert isinstance (excinfo .value , exceptions .InvalidArgumentError )
467
531
assert excinfo .value .http_response is None
468
532
469
533
def test_expired_token (self , user_mgt_app ):
534
+ _overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
470
535
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
471
536
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
472
543
with pytest .raises (auth .ExpiredIdTokenError ) as excinfo :
473
544
auth .verify_id_token (id_token , app = user_mgt_app )
474
545
assert isinstance (excinfo .value , auth .InvalidIdTokenError )
@@ -506,6 +577,10 @@ def test_custom_token(self, auth_app):
506
577
507
578
def test_certificate_request_failure (self , user_mgt_app ):
508
579
_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
509
584
with pytest .raises (auth .CertificateFetchError ) as excinfo :
510
585
auth .verify_id_token (TEST_ID_TOKEN , app = user_mgt_app )
511
586
assert 'Could not fetch certificates' in str (excinfo .value )
@@ -540,6 +615,12 @@ class TestVerifySessionCookie:
540
615
'IDToken' : TEST_ID_TOKEN ,
541
616
}
542
617
618
+ cookies_not_invalid_in_emulator = [
619
+ 'WrongKid' ,
620
+ 'FutureCookie' ,
621
+ 'ExpiredCookie'
622
+ ]
623
+
543
624
@pytest .mark .parametrize ('cookie' , valid_cookies .values (), ids = list (valid_cookies ))
544
625
def test_valid_cookie (self , user_mgt_app , cookie ):
545
626
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
@@ -578,8 +659,15 @@ def test_invalid_args(self, user_mgt_app, cookie):
578
659
auth .verify_session_cookie (cookie , app = user_mgt_app )
579
660
assert 'Illegal session cookie provided' in str (excinfo .value )
580
661
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
583
671
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
584
672
with pytest .raises (auth .InvalidSessionCookieError ) as excinfo :
585
673
auth .verify_session_cookie (cookie , app = user_mgt_app )
@@ -589,6 +677,12 @@ def test_invalid_cookie(self, user_mgt_app, cookie):
589
677
def test_expired_cookie (self , user_mgt_app ):
590
678
_overwrite_cert_request (user_mgt_app , MOCK_REQUEST )
591
679
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
592
686
with pytest .raises (auth .ExpiredSessionCookieError ) as excinfo :
593
687
auth .verify_session_cookie (cookie , app = user_mgt_app )
594
688
assert isinstance (excinfo .value , auth .InvalidSessionCookieError )
@@ -621,6 +715,10 @@ def test_custom_token(self, auth_app):
621
715
622
716
def test_certificate_request_failure (self , user_mgt_app ):
623
717
_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
624
722
with pytest .raises (auth .CertificateFetchError ) as excinfo :
625
723
auth .verify_session_cookie (TEST_SESSION_COOKIE , app = user_mgt_app )
626
724
assert 'Could not fetch certificates' in str (excinfo .value )
@@ -637,9 +735,11 @@ def test_certificate_caching(self, user_mgt_app, httpserver):
637
735
verifier .cookie_verifier .cert_url = httpserver .url
638
736
verifier .id_token_verifier .cert_url = httpserver .url
639
737
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
641
741
# Subsequent requests should not fetch certs from the server
642
742
verifier .verify_session_cookie (TEST_SESSION_COOKIE )
643
- assert len (httpserver .requests ) == 1
743
+ assert len (httpserver .requests ) == request_count
644
744
verifier .verify_id_token (TEST_ID_TOKEN )
645
- assert len (httpserver .requests ) == 1
745
+ assert len (httpserver .requests ) == request_count
0 commit comments