Skip to content

Commit 9d899e2

Browse files
committed
Support auth emulator via FIREBASE_AUTH_EMULATOR_HOST
Modeled on firebase/firebase-admin-go#414
1 parent c10c747 commit 9d899e2

File tree

7 files changed

+59
-24
lines changed

7 files changed

+59
-24
lines changed

firebase_admin/_auth_client.py

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

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

17+
import os
1718
import time
1819

1920
import firebase_admin
@@ -24,7 +25,10 @@
2425
from firebase_admin import _user_identifier
2526
from firebase_admin import _user_import
2627
from firebase_admin import _user_mgt
28+
from firebase_admin import _utils
2729

30+
_EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST'
31+
_DEFAULT_AUTH_URL = 'https://identitytoolkit.googleapis.com'
2832

2933
class Client:
3034
"""Firebase Authentication client scoped to a specific tenant."""
@@ -36,17 +40,38 @@ def __init__(self, app, tenant_id=None):
3640
2. set the project ID explicitly via Firebase App options, or
3741
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")
3842

39-
credential = app.credential.get_credential()
43+
credential = None
4044
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
45+
# Non-default endpoint URLs for emulator support are set in this dict later.
46+
endpoint_urls = {}
47+
48+
# If an emulator is present, check that the given value matches the expected format and set
49+
# endpoint URLs to use the emulator. Additionally, use a fake credential.
50+
emulator_host = os.environ.get(_EMULATOR_HOST_ENV_VAR)
51+
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))
56+
base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host)
57+
endpoint_urls['v1'] = base_url + '/v1'
58+
endpoint_urls['v2beta1'] = base_url + '/v2beta1'
59+
credential = _utils.EmulatorAdminCredentials()
60+
else:
61+
# Use credentials if provided
62+
credential = app.credential.get_credential()
63+
4164
http_client = _http_client.JsonHttpClient(
4265
credential=credential, headers={'X-Client-Version': version_header})
4366

4467
self._tenant_id = tenant_id
45-
self._token_generator = _token_gen.TokenGenerator(app, http_client)
68+
self._token_generator = _token_gen.TokenGenerator(
69+
app, http_client, url_override=endpoint_urls.get('v1'))
4670
self._token_verifier = _token_gen.TokenVerifier(app)
47-
self._user_manager = _user_mgt.UserManager(http_client, app.project_id, tenant_id)
71+
self._user_manager = _user_mgt.UserManager(
72+
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v1'))
4873
self._provider_manager = _auth_providers.ProviderConfigClient(
49-
http_client, app.project_id, tenant_id)
74+
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v2beta1'))
5075

5176
@property
5277
def tenant_id(self):

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/_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)