@@ -76,13 +76,19 @@ def _merge_jwt_claims(defaults, overrides):
7676
7777def 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 }])
145159def 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 }])
161175def 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