Skip to content

Commit 0720435

Browse files
committed
WIP hack integration tests for auth emulator
1 parent 3b930f0 commit 0720435

File tree

6 files changed

+90
-19
lines changed

6 files changed

+90
-19
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
run: |
3131
npm install -g firebase-tools
3232
firebase emulators:exec --only database --project fake-project-id 'pytest integration/test_db.py'
33+
echo mock-api-key > apikey.txt
34+
firebase emulators:exec --only auth --project mock-project-id 'pytest integration/test_auth.py --cert tests/data/service_account.json --apikey apikey.txt'
3335
3436
lint:
3537
runs-on: ubuntu-latest

firebase_admin/_auth_client.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
"""Firebase auth client sub module."""
1616

17-
import os
1817
import time
1918

2019
import firebase_admin
@@ -27,9 +26,6 @@
2726
from firebase_admin import _user_mgt
2827
from firebase_admin import _utils
2928

30-
_EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST'
31-
_DEFAULT_AUTH_URL = 'https://identitytoolkit.googleapis.com'
32-
3329
class Client:
3430
"""Firebase Authentication client scoped to a specific tenant."""
3531

@@ -44,19 +40,17 @@ def __init__(self, app, tenant_id=None):
4440
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
4541
# Non-default endpoint URLs for emulator support are set in this dict later.
4642
endpoint_urls = {}
43+
self.emulated = False
4744

4845
# If an emulator is present, check that the given value matches the expected format and set
4946
# endpoint URLs to use the emulator. Additionally, use a fake credential.
50-
emulator_host = os.environ.get(_EMULATOR_HOST_ENV_VAR)
47+
emulator_host = _auth_utils.get_emulator_host()
5148
if emulator_host:
52-
if '//' in emulator_host:
53-
raise ValueError(
54-
'Invalid {0}: "{1}". It must follow format "host:port".'.format(
55-
_EMULATOR_HOST_ENV_VAR, emulator_host))
5649
base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host)
5750
endpoint_urls['v1'] = base_url + '/v1'
5851
endpoint_urls['v2beta1'] = base_url + '/v2beta1'
5952
credential = _utils.EmulatorAdminCredentials()
53+
self.emulated = True
6054
else:
6155
# Use credentials if provided
6256
credential = app.credential.get_credential()
@@ -132,7 +126,7 @@ def verify_id_token(self, id_token, check_revoked=False):
132126
raise _auth_utils.TenantIdMismatchError(
133127
'Invalid tenant ID: {0}'.format(token_tenant_id))
134128

135-
if check_revoked:
129+
if not self.emulated and check_revoked:
136130
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
137131
return verified_claims
138132

firebase_admin/_auth_utils.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
"""Firebase auth utils."""
1616

1717
import json
18+
import os
1819
import re
1920
from urllib import parse
2021

2122
from firebase_admin import exceptions
2223
from firebase_admin import _utils
2324

24-
25+
EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST'
2526
MAX_CLAIMS_PAYLOAD_SIZE = 1000
2627
RESERVED_CLAIMS = set([
2728
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
@@ -66,6 +67,19 @@ def __iter__(self):
6667
return self
6768

6869

70+
def get_emulator_host():
71+
emulator_host = os.getenv(EMULATOR_HOST_ENV_VAR)
72+
if emulator_host and '//' in emulator_host:
73+
raise ValueError(
74+
'Invalid {0}: "{1}". It must follow format "host:port".'.format(
75+
EMULATOR_HOST_ENV_VAR, emulator_host))
76+
return emulator_host
77+
78+
79+
def is_emulated():
80+
return get_emulator_host() not in [None, '']
81+
82+
6983
def validate_uid(uid, required=False):
7084
if uid is None and not required:
7185
return None

firebase_admin/_token_gen.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from firebase_admin import _auth_utils
3232

3333

34+
#_auth_utils.is_emulated() = _auth_utils.get_emulator_host() != ''
3435
# ID token constants
3536
ID_TOKEN_ISSUER_PREFIX = 'https://securetoken.google.com/'
3637
ID_TOKEN_CERT_URI = ('https://www.googleapis.com/robot/v1/metadata/x509/'
@@ -54,6 +55,16 @@
5455
'service-accounts/default/email')
5556

5657

58+
class _EmulatedSigner(google.auth.crypt.Signer):
59+
key_id = None
60+
61+
def __init__(self):
62+
pass
63+
64+
def sign(self, message):
65+
return b''
66+
67+
5768
class _SigningProvider:
5869
"""Stores a reference to a google.auth.crypto.Signer."""
5970

@@ -78,6 +89,10 @@ def from_iam(cls, request, google_cred, service_account):
7889
signer = iam.Signer(request, google_cred, service_account)
7990
return _SigningProvider(signer, service_account)
8091

92+
@classmethod
93+
def for_emulator(cls):
94+
return _SigningProvider(_EmulatedSigner(), '[email protected]')
95+
8196

8297
class TokenGenerator:
8398
"""Generates custom tokens and session cookies."""
@@ -94,6 +109,8 @@ def __init__(self, app, http_client, url_override=None):
94109

95110
def _init_signing_provider(self):
96111
"""Initializes a signing provider by following the go/firebase-admin-sign protocol."""
112+
if _auth_utils.is_emulated():
113+
return _SigningProvider.for_emulator()
97114
# If the SDK was initialized with a service account, use it to sign bytes.
98115
google_cred = self.app.credential.get_credential()
99116
if isinstance(google_cred, google.oauth2.service_account.Credentials):
@@ -291,15 +308,15 @@ def verify(self, token, request):
291308
error_message = (
292309
'{0} expects {1}, but was given a custom '
293310
'token.'.format(self.operation, self.articled_short_name))
294-
elif not header.get('kid'):
311+
elif not _auth_utils.is_emulated() and not header.get('kid'):
295312
if header.get('alg') == 'HS256' and payload.get(
296313
'v') == 0 and 'uid' in payload.get('d', {}):
297314
error_message = (
298315
'{0} expects {1}, but was given a legacy custom '
299316
'token.'.format(self.operation, self.articled_short_name))
300317
else:
301318
error_message = 'Firebase {0} has no "kid" claim.'.format(self.short_name)
302-
elif header.get('alg') != 'RS256':
319+
elif not _auth_utils.is_emulated() and header.get('alg') != 'RS256':
303320
error_message = (
304321
'Firebase {0} has incorrect algorithm. Expected "RS256" but got '
305322
'"{1}". {2}'.format(self.short_name, header.get('alg'), verify_id_token_msg))
@@ -329,6 +346,10 @@ def verify(self, token, request):
329346
if error_message:
330347
raise self._invalid_token_error(error_message)
331348

349+
if _auth_utils.is_emulated():
350+
claims = jwt.decode(token, verify=False)
351+
claims['uid'] = claims['sub']
352+
return claims
332353
try:
333354
verified_claims = google.oauth2.id_token.verify_token(
334355
token,
@@ -342,7 +363,7 @@ def verify(self, token, request):
342363
except ValueError as error:
343364
if 'Token expired' in str(error):
344365
raise self._expired_token_error(str(error), cause=error)
345-
raise self._invalid_token_error(str(error), cause=error)
366+
raise self._invalid_token_error(str(error) + "FOO", cause=error)
346367

347368
def _decode_unverified(self, token):
348369
try:

integration/test_auth.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Integration tests for firebase_admin.auth module."""
1616
import base64
1717
import datetime
18+
import os
1819
import random
1920
import string
2021
import time
@@ -32,11 +33,18 @@
3233
from firebase_admin import credentials
3334

3435

35-
_verify_token_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken'
36-
_verify_password_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword'
37-
_password_reset_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPassword'
38-
_verify_email_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo'
39-
_email_sign_in_url = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/emailLinkSignin'
36+
_EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST'
37+
URL_PREFIX = 'https://www.googleapis.com/identitytoolkit'
38+
39+
emulator_host = os.getenv(_EMULATOR_HOST_ENV_VAR)
40+
if emulator_host:
41+
URL_PREFIX = 'http://{0}/www.googleapis.com/identitytoolkit'.format(emulator_host)
42+
43+
_verify_token_url = '{0}/v3/relyingparty/verifyCustomToken'.format(URL_PREFIX)
44+
_verify_password_url = '{0}/v3/relyingparty/verifyPassword'.format(URL_PREFIX)
45+
_password_reset_url = '{0}/v3/relyingparty/resetPassword'.format(URL_PREFIX)
46+
_verify_email_url = '{0}/v3/relyingparty/setAccountInfo'.format(URL_PREFIX)
47+
_email_sign_in_url = '{0}/v3/relyingparty/emailLinkSignin'.format(URL_PREFIX)
4048

4149
ACTION_LINK_CONTINUE_URL = 'http://localhost?a=1&b=5#f=1'
4250

@@ -560,6 +568,9 @@ def test_verify_id_token_revoked(new_user, api_key):
560568
# verify_id_token succeeded because it didn't check revoked.
561569
assert claims['iat'] * 1000 < user.tokens_valid_after_timestamp
562570

571+
if emulator_host:
572+
pytest.skip("Not supported with auth emulator")
573+
563574
with pytest.raises(auth.RevokedIdTokenError) as excinfo:
564575
claims = auth.verify_id_token(id_token, check_revoked=True)
565576
assert str(excinfo.value) == 'The Firebase ID token has been revoked.'

tests/test_token_gen.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def _merge_jwt_claims(defaults, overrides):
7575
return defaults
7676

7777
def verify_custom_token(custom_token, expected_claims, tenant_id=None):
78+
if os.getenv(EMULATOR_HOST_ENV_VAR):
79+
pytest.skip("Not supported with auth emulator")
7880
assert isinstance(custom_token, bytes)
7981
token = google.oauth2.id_token.verify_token(
8082
custom_token,
@@ -230,10 +232,14 @@ def test_invalid_params(self, auth_app, values):
230232
auth.create_custom_token(user, claims, app=auth_app)
231233

232234
def test_noncert_credential(self, user_mgt_app):
235+
if os.getenv(EMULATOR_HOST_ENV_VAR):
236+
pytest.skip("Not supported with auth emulator")
233237
with pytest.raises(ValueError):
234238
auth.create_custom_token(MOCK_UID, app=user_mgt_app)
235239

236240
def test_sign_with_iam(self):
241+
if os.getenv(EMULATOR_HOST_ENV_VAR):
242+
pytest.skip("Not supported with auth emulator")
237243
options = {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
238244
app = firebase_admin.initialize_app(
239245
testutils.MockCredential(), name='iam-signer-app', options=options)
@@ -248,6 +254,8 @@ def test_sign_with_iam(self):
248254
firebase_admin.delete_app(app)
249255

250256
def test_sign_with_iam_error(self):
257+
if os.getenv(EMULATOR_HOST_ENV_VAR):
258+
pytest.skip("Not supported with auth emulator")
251259
options = {'serviceAccountId': 'test-service-account', 'projectId': 'mock-project-id'}
252260
app = firebase_admin.initialize_app(
253261
testutils.MockCredential(), name='iam-signer-app', options=options)
@@ -264,6 +272,8 @@ def test_sign_with_iam_error(self):
264272
firebase_admin.delete_app(app)
265273

266274
def test_sign_with_discovered_service_account(self):
275+
if os.getenv(EMULATOR_HOST_ENV_VAR):
276+
pytest.skip("Not supported with auth emulator")
267277
request = testutils.MockRequest(200, 'discovered-service-account')
268278
options = {'projectId': 'mock-project-id'}
269279
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app',
@@ -287,6 +297,8 @@ def test_sign_with_discovered_service_account(self):
287297
firebase_admin.delete_app(app)
288298

289299
def test_sign_with_discovery_failure(self):
300+
if os.getenv(EMULATOR_HOST_ENV_VAR):
301+
pytest.skip("Not supported with auth emulator")
290302
request = testutils.MockFailedRequest(Exception('test error'))
291303
options = {'projectId': 'mock-project-id'}
292304
app = firebase_admin.initialize_app(testutils.MockCredential(), name='iam-signer-app',
@@ -431,6 +443,8 @@ def test_valid_token_check_revoked(self, user_mgt_app, id_token):
431443

432444
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
433445
def test_revoked_token_check_revoked(self, user_mgt_app, revoked_tokens, id_token):
446+
if os.getenv(EMULATOR_HOST_ENV_VAR):
447+
pytest.skip("Not supported with auth emulator")
434448
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
435449
_instrument_user_manager(user_mgt_app, 200, revoked_tokens)
436450
with pytest.raises(auth.RevokedIdTokenError) as excinfo:
@@ -460,13 +474,18 @@ def test_invalid_arg(self, user_mgt_app, id_token):
460474

461475
@pytest.mark.parametrize('id_token', invalid_tokens.values(), ids=list(invalid_tokens))
462476
def test_invalid_token(self, user_mgt_app, id_token):
477+
if os.getenv(EMULATOR_HOST_ENV_VAR):
478+
pytest.skip("Not supported with auth emulator")
463479
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
464480
with pytest.raises(auth.InvalidIdTokenError) as excinfo:
465481
auth.verify_id_token(id_token, app=user_mgt_app)
466482
assert isinstance(excinfo.value, exceptions.InvalidArgumentError)
467483
assert excinfo.value.http_response is None
468484

469485
def test_expired_token(self, user_mgt_app):
486+
if os.getenv(EMULATOR_HOST_ENV_VAR):
487+
pytest.skip("Not supported with auth emulator")
488+
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
470489
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
471490
id_token = self.invalid_tokens['ExpiredToken']
472491
with pytest.raises(auth.ExpiredIdTokenError) as excinfo:
@@ -505,6 +524,8 @@ def test_custom_token(self, auth_app):
505524
assert str(excinfo.value) == message
506525

507526
def test_certificate_request_failure(self, user_mgt_app):
527+
if os.getenv(EMULATOR_HOST_ENV_VAR):
528+
pytest.skip("Not supported with auth emulator")
508529
_overwrite_cert_request(user_mgt_app, testutils.MockRequest(404, 'not found'))
509530
with pytest.raises(auth.CertificateFetchError) as excinfo:
510531
auth.verify_id_token(TEST_ID_TOKEN, app=user_mgt_app)
@@ -580,13 +601,17 @@ def test_invalid_args(self, user_mgt_app, cookie):
580601

581602
@pytest.mark.parametrize('cookie', invalid_cookies.values(), ids=list(invalid_cookies))
582603
def test_invalid_cookie(self, user_mgt_app, cookie):
604+
if os.getenv(EMULATOR_HOST_ENV_VAR):
605+
pytest.skip("Not supported with auth emulator")
583606
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
584607
with pytest.raises(auth.InvalidSessionCookieError) as excinfo:
585608
auth.verify_session_cookie(cookie, app=user_mgt_app)
586609
assert isinstance(excinfo.value, exceptions.InvalidArgumentError)
587610
assert excinfo.value.http_response is None
588611

589612
def test_expired_cookie(self, user_mgt_app):
613+
if os.getenv(EMULATOR_HOST_ENV_VAR):
614+
pytest.skip("Not supported with auth emulator")
590615
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
591616
cookie = self.invalid_cookies['ExpiredCookie']
592617
with pytest.raises(auth.ExpiredSessionCookieError) as excinfo:
@@ -620,6 +645,8 @@ def test_custom_token(self, auth_app):
620645
auth.verify_session_cookie(custom_token, app=auth_app)
621646

622647
def test_certificate_request_failure(self, user_mgt_app):
648+
if os.getenv(EMULATOR_HOST_ENV_VAR):
649+
pytest.skip("Not supported with auth emulator")
623650
_overwrite_cert_request(user_mgt_app, testutils.MockRequest(404, 'not found'))
624651
with pytest.raises(auth.CertificateFetchError) as excinfo:
625652
auth.verify_session_cookie(TEST_SESSION_COOKIE, app=user_mgt_app)
@@ -632,6 +659,8 @@ def test_certificate_request_failure(self, user_mgt_app):
632659
class TestCertificateCaching:
633660

634661
def test_certificate_caching(self, user_mgt_app, httpserver):
662+
if os.getenv(EMULATOR_HOST_ENV_VAR):
663+
pytest.skip("Not supported with auth emulator")
635664
httpserver.serve_content(MOCK_PUBLIC_CERTS, 200, headers={'Cache-Control': 'max-age=3600'})
636665
verifier = _token_gen.TokenVerifier(user_mgt_app)
637666
verifier.cookie_verifier.cert_url = httpserver.url

0 commit comments

Comments
 (0)