[go: nahoru, domu]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): Add auth emulator support via the FIREBASE_AUTH_EMULATOR_HOST environment variable. #531

Merged
merged 5 commits into from
Mar 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from firebase_admin import _user_identifier
from firebase_admin import _user_import
from firebase_admin import _user_mgt
from firebase_admin import _utils


class Client:
Expand All @@ -36,18 +37,37 @@ def __init__(self, app, tenant_id=None):
2. set the project ID explicitly via Firebase App options, or
3. set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.""")

credential = app.credential.get_credential()
credential = None
version_header = 'Python/Admin/{0}'.format(firebase_admin.__version__)
timeout = app.options.get('httpTimeout', _http_client.DEFAULT_TIMEOUT_SECONDS)
# Non-default endpoint URLs for emulator support are set in this dict later.
endpoint_urls = {}
self.emulated = False

# If an emulator is present, check that the given value matches the expected format and set
# endpoint URLs to use the emulator. Additionally, use a fake credential.
emulator_host = _auth_utils.get_emulator_host()
if emulator_host:
base_url = 'http://{0}/identitytoolkit.googleapis.com'.format(emulator_host)
endpoint_urls['v1'] = base_url + '/v1'
endpoint_urls['v2beta1'] = base_url + '/v2beta1'
credential = _utils.EmulatorAdminCredentials()
self.emulated = True
else:
# Use credentials if provided
credential = app.credential.get_credential()

http_client = _http_client.JsonHttpClient(
credential=credential, headers={'X-Client-Version': version_header}, timeout=timeout)

self._tenant_id = tenant_id
self._token_generator = _token_gen.TokenGenerator(app, http_client)
self._token_generator = _token_gen.TokenGenerator(
hiranya911 marked this conversation as resolved.
Show resolved Hide resolved
app, http_client, url_override=endpoint_urls.get('v1'))
self._token_verifier = _token_gen.TokenVerifier(app)
self._user_manager = _user_mgt.UserManager(http_client, app.project_id, tenant_id)
self._user_manager = _user_mgt.UserManager(
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v1'))
self._provider_manager = _auth_providers.ProviderConfigClient(
http_client, app.project_id, tenant_id)
http_client, app.project_id, tenant_id, url_override=endpoint_urls.get('v2beta1'))

@property
def tenant_id(self):
Expand Down
5 changes: 3 additions & 2 deletions firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ class ProviderConfigClient:

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

def __init__(self, http_client, project_id, tenant_id=None):
def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
self.http_client = http_client
self.base_url = '{0}/projects/{1}'.format(self.PROVIDER_CONFIG_URL, project_id)
url_prefix = url_override or self.PROVIDER_CONFIG_URL
self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id)
if tenant_id:
self.base_url += '/tenants/{0}'.format(tenant_id)

Expand Down
15 changes: 15 additions & 0 deletions firebase_admin/_auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"""Firebase auth utils."""

import json
import os
import re
from urllib import parse

from firebase_admin import exceptions
from firebase_admin import _utils


EMULATOR_HOST_ENV_VAR = 'FIREBASE_AUTH_EMULATOR_HOST'
MAX_CLAIMS_PAYLOAD_SIZE = 1000
RESERVED_CLAIMS = set([
'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat',
Expand Down Expand Up @@ -66,6 +68,19 @@ def __iter__(self):
return self


def get_emulator_host():
emulator_host = os.getenv(EMULATOR_HOST_ENV_VAR, '')
if emulator_host and '//' in emulator_host:
raise ValueError(
'Invalid {0}: "{1}". It must follow format "host:port".'.format(
EMULATOR_HOST_ENV_VAR, emulator_host))
return emulator_host


def is_emulated():
return get_emulator_host() != ''


def validate_uid(uid, required=False):
if uid is None and not required:
return None
Expand Down
43 changes: 34 additions & 9 deletions firebase_admin/_token_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@
METADATA_SERVICE_URL = ('http://metadata.google.internal/computeMetadata/v1/instance/'
'service-accounts/default/email')

# Emulator fake account
AUTH_EMULATOR_EMAIL = 'firebase-auth-emulator@example.com'


class _EmulatedSigner(google.auth.crypt.Signer):
key_id = None

def __init__(self):
pass

def sign(self, message):
return b''


class _SigningProvider:
"""Stores a reference to a google.auth.crypto.Signer."""
Expand All @@ -78,21 +91,28 @@ def from_iam(cls, request, google_cred, service_account):
signer = iam.Signer(request, google_cred, service_account)
return _SigningProvider(signer, service_account)

@classmethod
def for_emulator(cls):
return _SigningProvider(_EmulatedSigner(), AUTH_EMULATOR_EMAIL)


class TokenGenerator:
"""Generates custom tokens and session cookies."""

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

def __init__(self, app, http_client):
def __init__(self, app, http_client, url_override=None):
self.app = app
self.http_client = http_client
self.request = transport.requests.Request()
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, app.project_id)
url_prefix = url_override or self.ID_TOOLKIT_URL
self.base_url = '{0}/projects/{1}'.format(url_prefix, app.project_id)
self._signing_provider = None

def _init_signing_provider(self):
"""Initializes a signing provider by following the go/firebase-admin-sign protocol."""
if _auth_utils.is_emulated():
return _SigningProvider.for_emulator()
# If the SDK was initialized with a service account, use it to sign bytes.
google_cred = self.app.credential.get_credential()
if isinstance(google_cred, google.oauth2.service_account.Credentials):
Expand Down Expand Up @@ -285,20 +305,22 @@ def verify(self, token, request):
verify_id_token_msg = (
'See {0} for details on how to retrieve {1}.'.format(self.url, self.short_name))

emulated = _auth_utils.is_emulated()

error_message = None
if audience == FIREBASE_AUDIENCE:
error_message = (
'{0} expects {1}, but was given a custom '
'token.'.format(self.operation, self.articled_short_name))
elif not header.get('kid'):
elif not emulated and not header.get('kid'):
if header.get('alg') == 'HS256' and payload.get(
'v') == 0 and 'uid' in payload.get('d', {}):
error_message = (
'{0} expects {1}, but was given a legacy custom '
'token.'.format(self.operation, self.articled_short_name))
else:
error_message = 'Firebase {0} has no "kid" claim.'.format(self.short_name)
elif header.get('alg') != 'RS256':
elif not emulated and header.get('alg') != 'RS256':
error_message = (
'Firebase {0} has incorrect algorithm. Expected "RS256" but got '
'"{1}". {2}'.format(self.short_name, header.get('alg'), verify_id_token_msg))
Expand Down Expand Up @@ -329,11 +351,14 @@ def verify(self, token, request):
raise self._invalid_token_error(error_message)

try:
verified_claims = google.oauth2.id_token.verify_token(
token,
request=request,
audience=self.project_id,
certs_url=self.cert_url)
if emulated:
verified_claims = payload
else:
verified_claims = google.oauth2.id_token.verify_token(
token,
request=request,
audience=self.project_id,
certs_url=self.cert_url)
verified_claims['uid'] = verified_claims['sub']
return verified_claims
except google.auth.exceptions.TransportError as error:
Expand Down
5 changes: 3 additions & 2 deletions firebase_admin/_user_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,10 @@ class UserManager:

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

def __init__(self, http_client, project_id, tenant_id=None):
def __init__(self, http_client, project_id, tenant_id=None, url_override=None):
self.http_client = http_client
self.base_url = '{0}/projects/{1}'.format(self.ID_TOOLKIT_URL, project_id)
url_prefix = url_override or self.ID_TOOLKIT_URL
self.base_url = '{0}/projects/{1}'.format(url_prefix, project_id)
if tenant_id:
self.base_url += '/tenants/{0}'.format(tenant_id)

Expand Down
18 changes: 18 additions & 0 deletions firebase_admin/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import json
import socket

import google.auth
import googleapiclient
import httplib2
import requests
Expand Down Expand Up @@ -339,3 +340,20 @@ def _parse_platform_error(content, status_code):
if not msg:
msg = 'Unexpected HTTP response with status: {0}; body: {1}'.format(status_code, content)
return error_dict, msg


# Temporarily disable the lint rule. For more information see:
# https://github.com/googleapis/google-auth-library-python/pull/561
# pylint: disable=abstract-method
class EmulatorAdminCredentials(google.auth.credentials.Credentials):
""" Credentials for use with the firebase local emulator.

This is used instead of user-supplied credentials or ADC. It will silently do nothing when
asked to refresh credentials.
"""
def __init__(self):
google.auth.credentials.Credentials.__init__(self)
self.token = 'owner'

def refresh(self, request):
pass
14 changes: 1 addition & 13 deletions firebase_admin/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import threading
from urllib import parse

import google.auth
import requests

import firebase_admin
Expand Down Expand Up @@ -808,7 +807,7 @@ def get_client(self, db_url=None):

emulator_config = self._get_emulator_config(parsed_url)
if emulator_config:
credential = _EmulatorAdminCredentials()
credential = _utils.EmulatorAdminCredentials()
base_url = emulator_config.base_url
params = {'ns': emulator_config.namespace}
else:
Expand Down Expand Up @@ -965,14 +964,3 @@ def _extract_error_message(cls, response):
message = 'Unexpected response from database: {0}'.format(response.content.decode())

return message

# Temporarily disable the lint rule. For more information see:
# https://github.com/googleapis/google-auth-library-python/pull/561
# pylint: disable=abstract-method
class _EmulatorAdminCredentials(google.auth.credentials.Credentials):
def __init__(self):
google.auth.credentials.Credentials.__init__(self)
self.token = 'owner'

def refresh(self, request):
pass
Loading