From 40f01260b9ebcfa5be7eda298693f58f3c06fd43 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 25 Mar 2020 13:10:48 -0700 Subject: [PATCH 1/3] Tenant-aware ID token verification support --- firebase_admin/_auth_utils.py | 7 +++++++ firebase_admin/auth.py | 7 +++++++ firebase_admin/tenant_mgt.py | 3 +++ tests/test_tenant_mgt.py | 27 +++++++++++++++++++++++++++ tests/test_token_gen.py | 16 ++++++++++++++++ 5 files changed, 60 insertions(+) diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index 0b91fe46e..2b2da61b8 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -300,6 +300,13 @@ def __init__(self, message, cause=None, http_response=None): exceptions.NotFoundError.__init__(self, message, cause, http_response) +class TenantIdMismatchError(InvalidIdTokenError): + """Missing or invalid tenant ID field in the given ID token.""" + + def __init__(self, message): + InvalidIdTokenError.__init__(self, message) + + _CODE_TO_EXC_TYPE = { 'DUPLICATE_EMAIL': EmailAlreadyExistsError, 'DUPLICATE_LOCAL_ID': UidAlreadyExistsError, diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 0e738bf86..2d215d3ae 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -22,6 +22,7 @@ import time import firebase_admin +from firebase_admin import exceptions from firebase_admin import _auth_utils from firebase_admin import _http_client from firebase_admin import _token_gen @@ -565,6 +566,12 @@ def verify_id_token(self, id_token, check_revoked=False): ' bool, but given "{0}".'.format(type(check_revoked))) verified_claims = self._token_verifier.verify_id_token(id_token) + if self.tenant_id: + token_tenant_id = verified_claims.get('firebase', {}).get('tenant') + if self.tenant_id != token_tenant_id: + raise _auth_utils.TenantIdMismatchError( + 'Invalid tenant ID: {0}'.format(token_tenant_id)) + if check_revoked: self._check_jwt_revoked(verified_claims, RevokedIdTokenError, 'ID token') return verified_claims diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 8d69e1db5..2139be018 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -36,6 +36,7 @@ __all__ = [ 'ListTenantsPage', 'Tenant', + 'TenantIdMismatchError', 'TenantNotFoundError', 'auth_for_tenant', @@ -46,6 +47,8 @@ 'update_tenant', ] + +TenantIdMismatchError = _auth_utils.TenantIdMismatchError TenantNotFoundError = _auth_utils.TenantNotFoundError diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 03ff3f0ab..03f954d1b 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -24,6 +24,7 @@ from firebase_admin import exceptions from firebase_admin import tenant_mgt from tests import testutils +from tests import test_token_gen GET_TENANT_RESPONSE = """{ @@ -706,6 +707,32 @@ def _assert_request(self, recorder, want_url, want_body): assert body == want_body +class TestVerifyIdToken: + + def test_valid_token(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('test-tenant', app=tenant_mgt_app) + client._token_verifier.request = test_token_gen.MOCK_REQUEST + + claims = client.verify_id_token(test_token_gen.TEST_ID_TOKEN_WITH_TENANT) + + assert claims['admin'] is True + assert claims['uid'] == claims['sub'] + assert claims['firebase']['tenant'] == 'test-tenant' + + def test_invalid_tenant_id(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('other-tenant', app=tenant_mgt_app) + client._token_verifier.request = test_token_gen.MOCK_REQUEST + + with pytest.raises(tenant_mgt.TenantIdMismatchError) as excinfo: + client.verify_id_token(test_token_gen.TEST_ID_TOKEN_WITH_TENANT) + + assert 'Invalid tenant ID: test-tenant' in str(excinfo.value) + assert isinstance(excinfo.value, auth.InvalidIdTokenError) + assert isinstance(excinfo.value, exceptions.InvalidArgumentError) + assert excinfo.value.cause is None + assert excinfo.value.http_response is None + + def _assert_tenant(tenant, tenant_id='tenant-id'): assert isinstance(tenant, tenant_mgt.Tenant) assert tenant.tenant_id == tenant_id diff --git a/tests/test_token_gen.py b/tests/test_token_gen.py index d677b740a..bbb6ef4e4 100644 --- a/tests/test_token_gen.py +++ b/tests/test_token_gen.py @@ -94,6 +94,9 @@ def _get_id_token(payload_overrides=None, header_overrides=None): 'exp': int(time.time()) + 3600, 'sub': '1234567890', 'admin': True, + 'firebase': { + 'sign_in_provider': 'provider', + }, } if header_overrides: headers = _merge_jwt_claims(headers, header_overrides) @@ -346,6 +349,11 @@ def test_unexpected_response(self, user_mgt_app): MOCK_GET_USER_RESPONSE = testutils.resource('get_user.json') TEST_ID_TOKEN = _get_id_token() +TEST_ID_TOKEN_WITH_TENANT = _get_id_token({ + 'firebase': { + 'tenant': 'test-tenant', + } +}) TEST_SESSION_COOKIE = _get_session_cookie() @@ -380,6 +388,14 @@ def test_valid_token(self, user_mgt_app, id_token): claims = auth.verify_id_token(id_token, app=user_mgt_app) assert claims['admin'] is True assert claims['uid'] == claims['sub'] + assert claims['firebase']['sign_in_provider'] == 'provider' + + def test_valid_token_with_tenant(self, user_mgt_app): + _overwrite_cert_request(user_mgt_app, MOCK_REQUEST) + claims = auth.verify_id_token(TEST_ID_TOKEN_WITH_TENANT, app=user_mgt_app) + assert claims['admin'] is True + assert claims['uid'] == claims['sub'] + assert claims['firebase']['tenant'] == 'test-tenant' @pytest.mark.parametrize('id_token', valid_tokens.values(), ids=list(valid_tokens)) def test_valid_token_check_revoked(self, user_mgt_app, id_token): From 6052890b0c263d4f224a262132b2ecb7744943f8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 25 Mar 2020 13:47:49 -0700 Subject: [PATCH 2/3] Extended InvalidArgumentError in TenantIdMismatchError --- firebase_admin/_auth_utils.py | 6 +++--- tests/test_tenant_mgt.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index 2b2da61b8..95e7f2718 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -300,11 +300,11 @@ def __init__(self, message, cause=None, http_response=None): exceptions.NotFoundError.__init__(self, message, cause, http_response) -class TenantIdMismatchError(InvalidIdTokenError): - """Missing or invalid tenant ID field in the given ID token.""" +class TenantIdMismatchError(exceptions.InvalidArgumentError): + """Missing or invalid tenant ID field in the given JWT.""" def __init__(self, message): - InvalidIdTokenError.__init__(self, message) + exceptions.InvalidArgumentError.__init__(self, message) _CODE_TO_EXC_TYPE = { diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 03f954d1b..4a765769f 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -697,7 +697,6 @@ def test_tenant_not_found(self, tenant_mgt_app): assert excinfo.value.http_response is not None assert excinfo.value.cause is not None - def _assert_request(self, recorder, want_url, want_body): assert len(recorder) == 1 req = recorder[0] @@ -727,7 +726,6 @@ def test_invalid_tenant_id(self, tenant_mgt_app): client.verify_id_token(test_token_gen.TEST_ID_TOKEN_WITH_TENANT) assert 'Invalid tenant ID: test-tenant' in str(excinfo.value) - assert isinstance(excinfo.value, auth.InvalidIdTokenError) assert isinstance(excinfo.value, exceptions.InvalidArgumentError) assert excinfo.value.cause is None assert excinfo.value.http_response is None From 07e9cb6f5255f3511fc61f8a31599e6e040b25fc Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 26 Mar 2020 13:11:39 -0700 Subject: [PATCH 3/3] Fixing lint errors --- firebase_admin/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 2d215d3ae..4b80bc05e 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -22,7 +22,6 @@ import time import firebase_admin -from firebase_admin import exceptions from firebase_admin import _auth_utils from firebase_admin import _http_client from firebase_admin import _token_gen @@ -560,6 +559,7 @@ def create_custom_token(self, uid, developer_claims=None): return self._token_generator.create_custom_token(uid, developer_claims) def verify_id_token(self, id_token, check_revoked=False): + """Verifies the signature and data for the provided ID token.""" if not isinstance(check_revoked, bool): # guard against accidental wrong assignment. raise ValueError('Illegal check_revoked argument. Argument must be of type '