Skip to content

Commit d5020ef

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

File tree

6 files changed

+59
-17
lines changed

6 files changed

+59
-17
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 & 8 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,7 +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'
3129
_DEFAULT_AUTH_URL = 'https://identitytoolkit.googleapis.com'
3230

3331
class Client:
@@ -44,19 +42,17 @@ def __init__(self, app, tenant_id=None):
4442
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
4543
# Non-default endpoint URLs for emulator support are set in this dict later.
4644
endpoint_urls = {}
45+
self.emulated = False
4746

4847
# If an emulator is present, check that the given value matches the expected format and set
4948
# endpoint URLs to use the emulator. Additionally, use a fake credential.
50-
emulator_host = os.environ.get(_EMULATOR_HOST_ENV_VAR)
49+
emulator_host = _auth_utils.get_emulator_host()
5150
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))
5651
base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host)
5752
endpoint_urls['v1'] = base_url + '/v1'
5853
endpoint_urls['v2beta1'] = base_url + '/v2beta1'
5954
credential = _utils.EmulatorAdminCredentials()
55+
self.emulated = True
6056
else:
6157
# Use credentials if provided
6258
credential = app.credential.get_credential()
@@ -132,7 +128,7 @@ def verify_id_token(self, id_token, check_revoked=False):
132128
raise _auth_utils.TenantIdMismatchError(
133129
'Invalid tenant ID: {0}'.format(token_tenant_id))
134130

135-
if check_revoked:
131+
if not self.emulated and check_revoked:
136132
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
137133
return verified_claims
138134

firebase_admin/_auth_utils.py

Lines changed: 11 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,15 @@ def __iter__(self):
6667
return self
6768

6869

70+
def get_emulator_host():
71+
emulator_host = os.getenv(EMULATOR_HOST_ENV_VAR)
72+
if '//' 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+
6979
def validate_uid(uid, required=False):
7080
if uid is None and not required:
7181
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+
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 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 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 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 IS_EMULATED:
350+
unverified_claims = jwt.decode(token, verify=False)
351+
unverified_claims['uid'] = unverified_claims['user_id']
352+
return unverified_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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ def test_valid_token_check_revoked(self, user_mgt_app, id_token):
431431

432432
@pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens))
433433
def test_revoked_token_check_revoked(self, user_mgt_app, revoked_tokens, id_token):
434+
if os.getenv(EMULATOR_HOST_ENV_VAR):
435+
pytest.skip("Not supported with auth emulator")
434436
_overwrite_cert_request(user_mgt_app, MOCK_REQUEST)
435437
_instrument_user_manager(user_mgt_app, 200, revoked_tokens)
436438
with pytest.raises(auth.RevokedIdTokenError) as excinfo:

0 commit comments

Comments
 (0)