Skip to content

Commit d01ec61

Browse files
committed
Support auth emulator via FIREBASE_AUTH_EMULATOR_HOST
Modeled on firebase/firebase-admin-go#414
1 parent 32e45f1 commit d01ec61

File tree

8 files changed

+70
-27
lines changed

8 files changed

+70
-27
lines changed

firebase_admin/_auth_client.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from firebase_admin import _user_identifier
2525
from firebase_admin import _user_import
2626
from firebase_admin import _user_mgt
27-
27+
from firebase_admin import _utils
2828

2929
class Client:
3030
"""Firebase Authentication client scoped to a specific tenant."""
@@ -36,18 +36,37 @@ def __init__(self, app, tenant_id=None):
3636
2. set the project ID explicitly via Firebase App options, or
3737
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")
3838

39-
credential = app.credential.get_credential()
39+
credential = None
4040
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
4141
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
42+
# Non-default endpoint URLs for emulator support are set in this dict later.
43+
endpoint_urls = {}
44+
self.emulated = False
45+
46+
# If an emulator is present, check that the given value matches the expected format and set
47+
# endpoint URLs to use the emulator. Additionally, use a fake credential.
48+
emulator_host = _auth_utils.get_emulator_host()
49+
if emulator_host:
50+
base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host)
51+
endpoint_urls['v1'] = base_url + '/v1'
52+
endpoint_urls['v2beta1'] = base_url + '/v2beta1'
53+
credential = _utils.EmulatorAdminCredentials()
54+
self.emulated = True
55+
else:
56+
# Use credentials if provided
57+
credential = app.credential.get_credential()
58+
4259
http_client = _http_client.JsonHttpClient(
4360
credential=credential, headers={'X-Client-Version': version_header}, timeout=timeout)
4461

4562
self._tenant_id = tenant_id
46-
self._token_generator = _token_gen.TokenGenerator(app, http_client)
63+
self._token_generator = _token_gen.TokenGenerator(
64+
app, http_client, url_override=endpoint_urls.get('v1'))
4765
self._token_verifier = _token_gen.TokenVerifier(app)
48-
self._user_manager = _user_mgt.UserManager(http_client, app.project_id, tenant_id)
66+
self._user_manager = _user_mgt.UserManager(
67+
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v1'))
4968
self._provider_manager = _auth_providers.ProviderConfigClient(
50-
http_client, app.project_id, tenant_id)
69+
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v2beta1'))
5170

5271
@property
5372
def tenant_id(self):
@@ -108,7 +127,7 @@ def verify_id_token(self, id_token, check_revoked=False):
108127
raise _auth_utils.TenantIdMismatchError(
109128
'Invalid tenant ID: {0}'.format(token_tenant_id))
110129

111-
if check_revoked:
130+
if not self.emulated and check_revoked:
112131
self._check_jwt_revoked(verified_claims, _token_gen.RevokedIdTokenError, 'ID token')
113132
return verified_claims
114133

firebase_admin/_auth_providers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,10 @@ class ProviderConfigClient:
166166

167167
PROVIDER_CONFIG_URL = 'https://identitytoolkit.googleapis.com/v2beta1'
168168

169-
def __init__(self, http_client, project_id, tenant_id=None):
169+
def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
170170
self.http_client = http_client
171-
self.base_url = '{0}/projects/{1}'.format(self.PROVIDER_CONFIG_URL, project_id)
171+
url_prefix = url_override or self.PROVIDER_CONFIG_URL
172+
self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id)
172173
if tenant_id:
173174
self.base_url += '/tenants/{0}'.format(tenant_id)
174175

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() != ''
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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ class TokenGenerator:
8484

8585
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
8686

87-
def __init__(self, app, http_client):
87+
def __init__(self, app, http_client, url_override=None):
8888
self.app = app
8989
self.http_client = http_client
9090
self.request = transport.requests.Request()
91-
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, app.project_id)
91+
url_prefix = url_override or self.ID_TOOLKIT_URL
92+
self.base_url = '{0}/projects/{1}'.format(url_prefix, app.project_id)
9293
self._signing_provider = None
9394

9495
def _init_signing_provider(self):

firebase_admin/_user_mgt.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,10 @@ class UserManager:
573573

574574
ID_TOOLKIT_URL = 'https://identitytoolkit.googleapis.com/v1'
575575

576-
def __init__(self, http_client, project_id, tenant_id=None):
576+
def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
577577
self.http_client = http_client
578-
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, project_id)
578+
url_prefix = url_override or self.ID_TOOLKIT_URL
579+
self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id)
579580
if tenant_id:
580581
self.base_url += '/tenants/{0}'.format(tenant_id)
581582

firebase_admin/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import json
1919
import socket
2020

21+
import google.auth
2122
import googleapiclient
2223
import httplib2
2324
import requests
@@ -339,3 +340,20 @@ def _parse_platform_error(content, status_code):
339340
if not msg:
340341
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content)
341342
return error_dict, msg
343+
344+
345+
# Temporarily disable the lint rule. For more information see:
346+
# https://github.com/googleapis/google-auth-library-python/pull/561
347+
# pylint: disable=abstract-method
348+
class EmulatorAdminCredentials(google.auth.credentials.Credentials):
349+
""" Credentials for use with the firebase local emulator.
350+
351+
This is used instead of user-supplied credentials or ADC. It will silently do nothing when
352+
asked to refresh credentials.
353+
"""
354+
def __init__(self):
355+
google.auth.credentials.Credentials.__init__(self)
356+
self.token = 'owner'
357+
358+
def refresh(self, request):
359+
pass

firebase_admin/db.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import threading
2828
from urllib import parse
2929

30-
import google.auth
3130
import requests
3231

3332
import firebase_admin
@@ -808,7 +807,7 @@ def get_client(self, db_url=None):
808807

809808
emulator_config = self._get_emulator_config(parsed_url)
810809
if emulator_config:
811-
credential = _EmulatorAdminCredentials()
810+
credential = _utils.EmulatorAdminCredentials()
812811
base_url = emulator_config.base_url
813812
params = {'ns': emulator_config.namespace}
814813
else:
@@ -965,14 +964,3 @@ def _extract_error_message(cls, response):
965964
message = 'Unexpected response from database: {0}'.format(response.content.decode())
966965

967966
return message
968-
969-
# Temporarily disable the lint rule. For more information see:
970-
# https://github.com/googleapis/google-auth-library-python/pull/561
971-
# pylint: disable=abstract-method
972-
class _EmulatorAdminCredentials(google.auth.credentials.Credentials):
973-
def __init__(self):
974-
google.auth.credentials.Credentials.__init__(self)
975-
self.token = 'owner'
976-
977-
def refresh(self, request):
978-
pass

tests/test_db.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from firebase_admin import exceptions
2727
from firebase_admin import _http_client
2828
from firebase_admin import _sseclient
29+
from firebase_admin import _utils
2930
from tests import testutils
3031

3132

@@ -730,7 +731,7 @@ def test_parse_db_url(self, url, emulator_host, expected_base_url, expected_name
730731
assert ref._client._base_url == expected_base_url
731732
assert ref._client.params.get('ns') == expected_namespace
732733
if expected_base_url.startswith('http://localhost'):
733-
assert isinstance(ref._client.credential, db._EmulatorAdminCredentials)
734+
assert isinstance(ref._client.credential, _utils.EmulatorAdminCredentials)
734735
else:
735736
assert isinstance(ref._client.credential, testutils.MockGoogleCredential)
736737
finally:

0 commit comments

Comments
 (0)