From 2249f19bffb3658d07a2f3649fe01c08e67e8b2f Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 6 Apr 2020 13:21:44 -0700 Subject: [PATCH 1/2] feat(auth): Added OIDCProviderConfig type and get/delete APIs --- firebase_admin/_auth_client.py | 29 ++++++++++++++ firebase_admin/_auth_providers.py | 37 ++++++++++++++++- firebase_admin/auth.py | 36 +++++++++++++++++ tests/data/oidc_provider_config.json | 7 ++++ tests/test_auth_providers.py | 59 ++++++++++++++++++++++++++++ tests/test_tenant_mgt.py | 36 ++++++++++++++++- 6 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 tests/data/oidc_provider_config.json diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 761c1a1f7..2d2b21437 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -390,6 +390,35 @@ def generate_sign_in_with_email_link(self, email, action_code_settings): return self._user_manager.generate_email_action_link( 'EMAIL_SIGNIN', email, action_code_settings=action_code_settings) + def get_oidc_provider_config(self, provider_id): + """Returns the OIDCProviderConfig with the given ID. + + Args: + provider_id: Provider ID string. + + Returns: + SAMLProviderConfig: An OIDCProviderConfig instance. + + Raises: + ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix. + ConfigurationNotFoundError: If no OIDC provider is available with the given identifier. + FirebaseError: If an error occurs while retrieving the OIDC provider. + """ + return self._provider_manager.get_oidc_provider_config(provider_id) + + def delete_oidc_provider_config(self, provider_id): + """Deletes the OIDCProviderConfig with the given ID. + + Args: + provider_id: Provider ID string. + + Raises: + ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix. + ConfigurationNotFoundError: If no OIDC provider is available with the given identifier. + FirebaseError: If an error occurs while deleting the OIDC provider. + """ + self._provider_manager.delete_oidc_provider_config(provider_id) + def get_saml_provider_config(self, provider_id): """Returns the SAMLProviderConfig with the given ID. diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 9bcb7cc4b..309b2ed1f 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -45,10 +45,26 @@ def enabled(self): return self._data['enabled'] +class OIDCProviderConfig(ProviderConfig): + """Represents the OIDC auth provider configuration. + + See https://openid.net/specs/openid-connect-core-1_0-final.html. + """ + + @property + def issuer(self): + return self._data['issuer'] + + @property + def client_id(self): + return self._data['clientId'] + + class SAMLProviderConfig(ProviderConfig): """Represents he SAML auth provider configuration. - See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html.""" + See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html. + """ @property def idp_entity_id(self): @@ -149,6 +165,15 @@ def __init__(self, http_client, project_id, tenant_id=None): if tenant_id: self.base_url += '/tenants/{0}'.format(tenant_id) + def get_oidc_provider_config(self, provider_id): + _validate_oidc_provider_id(provider_id) + body = self._make_request('get', '/oauthIdpConfigs/{0}'.format(provider_id)) + return OIDCProviderConfig(body) + + def delete_oidc_provider_config(self, provider_id): + _validate_oidc_provider_id(provider_id) + self._make_request('delete', '/oauthIdpConfigs/{0}'.format(provider_id)) + def get_saml_provider_config(self, provider_id): _validate_saml_provider_id(provider_id) body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id)) @@ -253,6 +278,16 @@ def _make_request(self, method, path, **kwargs): raise _auth_utils.handle_auth_backend_error(error) +def _validate_oidc_provider_id(provider_id): + if not isinstance(provider_id, str): + raise ValueError( + 'Invalid OIDC provider ID: {0}. Provider ID must be a non-empty string.'.format( + provider_id)) + if not provider_id.startswith('oidc.'): + raise ValueError('Invalid OIDC provider ID: {0}.'.format(provider_id)) + return provider_id + + def _validate_saml_provider_id(provider_id): if not isinstance(provider_id, str): raise ValueError( diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index 7d11bd58c..536be23a2 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -105,6 +105,7 @@ InvalidSessionCookieError = _token_gen.InvalidSessionCookieError ListProviderConfigsPage = _auth_providers.ListProviderConfigsPage ListUsersPage = _user_mgt.ListUsersPage +OIDCProviderConfig = _auth_providers.OIDCProviderConfig PhoneNumberAlreadyExistsError = _auth_utils.PhoneNumberAlreadyExistsError ProviderConfig = _auth_providers.ProviderConfigClient RevokedIdTokenError = _token_gen.RevokedIdTokenError @@ -545,6 +546,41 @@ def generate_sign_in_with_email_link(email, action_code_settings, app=None): email, action_code_settings=action_code_settings) +def get_oidc_provider_config(provider_id, app=None): + """Returns the OIDCProviderConfig with the given ID. + + Args: + provider_id: Provider ID string. + app: An App instance (optional). + + Returns: + OIDCProviderConfig: An OIDCProviderConfig instance. + + Raises: + ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix. + ConfigurationNotFoundError: If no OIDC provider is available with the given identifier. + FirebaseError: If an error occurs while retrieving the OIDC provider. + """ + client = _get_client(app) + return client.get_oidc_provider_config(provider_id) + + +def delete_oidc_provider_config(provider_id, app=None): + """Deletes the OIDCProviderConfig with the given ID. + + Args: + provider_id: Provider ID string. + app: An App instance (optional). + + Raises: + ValueError: If the provider ID is invalid, empty or does not have ``oidc.`` prefix. + ConfigurationNotFoundError: If no OIDC provider is available with the given identifier. + FirebaseError: If an error occurs while deleting the OIDC provider. + """ + client = _get_client(app) + client.delete_oidc_provider_config(provider_id) + + def get_saml_provider_config(provider_id, app=None): """Returns the SAMLProviderConfig with the given ID. diff --git a/tests/data/oidc_provider_config.json b/tests/data/oidc_provider_config.json new file mode 100644 index 000000000..2c021424c --- /dev/null +++ b/tests/data/oidc_provider_config.json @@ -0,0 +1,7 @@ +{ + "name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider", + "clientId": "CLIENT_ID", + "issuer": "https://oidc.com/issuer", + "displayName": "oidcProviderName", + "enabled": true +} \ No newline at end of file diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index f5a66a7c5..531dcebea 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -25,6 +25,7 @@ from tests import testutils USER_MGT_URL_PREFIX = 'https://identitytoolkit.googleapis.com/v2beta1/projects/mock-project-id' +OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json') SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json') LIST_SAML_PROVIDER_CONFIGS_RESPONSE = testutils.resource('list_saml_provider_configs.json') @@ -55,6 +56,64 @@ def _instrument_provider_mgt(app, status, payload): return recorder +class TestOIDCProviderConfig: + + @pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider']) + def test_get_invalid_provider_id(self, user_mgt_app, provider_id): + with pytest.raises(ValueError) as excinfo: + auth.get_oidc_provider_config(provider_id, app=user_mgt_app) + + assert str(excinfo.value).startswith('Invalid OIDC provider ID') + + def test_get(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = auth.get_oidc_provider_config('oidc.provider', app=user_mgt_app) + + self._assert_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider') + + @pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider']) + def test_delete_invalid_provider_id(self, user_mgt_app, provider_id): + with pytest.raises(ValueError) as excinfo: + auth.delete_oidc_provider_config(provider_id, app=user_mgt_app) + + assert str(excinfo.value).startswith('Invalid OIDC provider ID') + + def test_delete(self, user_mgt_app): + recorder = _instrument_provider_mgt(user_mgt_app, 200, '{}') + + auth.delete_oidc_provider_config('oidc.provider', app=user_mgt_app) + + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'DELETE' + assert req.url == '{0}{1}'.format(USER_MGT_URL_PREFIX, '/oauthIdpConfigs/oidc.provider') + + def test_config_not_found(self, user_mgt_app): + _instrument_provider_mgt(user_mgt_app, 500, CONFIG_NOT_FOUND_RESPONSE) + + with pytest.raises(auth.ConfigurationNotFoundError) as excinfo: + auth.get_oidc_provider_config('oidc.provider', app=user_mgt_app) + + error_msg = 'No auth provider found for the given identifier (CONFIGURATION_NOT_FOUND).' + assert excinfo.value.code == exceptions.NOT_FOUND + assert str(excinfo.value) == error_msg + assert excinfo.value.http_response is not None + assert excinfo.value.cause is not None + + def _assert_provider_config(self, provider_config, want_id='oidc.provider'): + assert isinstance(provider_config, auth.OIDCProviderConfig) + assert provider_config.provider_id == want_id + assert provider_config.display_name == 'oidcProviderName' + assert provider_config.enabled is True + assert provider_config.issuer == 'https://oidc.com/issuer' + assert provider_config.client_id == 'CLIENT_ID' + + class TestSAMLProviderConfig: VALID_CREATE_OPTIONS = { diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 7cb8e7bab..06b1064fe 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -78,6 +78,7 @@ MOCK_GET_USER_RESPONSE = testutils.resource('get_user.json') MOCK_LIST_USERS_RESPONSE = testutils.resource('list_users.json') +OIDC_PROVIDER_CONFIG_RESPONSE = testutils.resource('oidc_provider_config.json') SAML_PROVIDER_CONFIG_RESPONSE = testutils.resource('saml_provider_config.json') SAML_PROVIDER_CONFIG_REQUEST = body = { 'displayName': 'samlProviderName', @@ -715,6 +716,31 @@ def test_generate_sign_in_with_email_link(self, tenant_mgt_app): 'continueUrl': 'http://localhost', }) + def test_get_oidc_provider_config(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) + recorder = _instrument_provider_mgt(client, 200, OIDC_PROVIDER_CONFIG_RESPONSE) + + provider_config = client.get_oidc_provider_config('oidc.provider') + + self._assert_oidc_provider_config(provider_config) + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'GET' + assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( + PROVIDER_MGT_URL_PREFIX) + + def test_delete_oidc_provider_config(self, tenant_mgt_app): + client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) + recorder = _instrument_provider_mgt(client, 200, '{}') + + client.delete_oidc_provider_config('oidc.provider') + + assert len(recorder) == 1 + req = recorder[0] + assert req.method == 'DELETE' + assert req.url == '{0}/tenants/tenant-id/oauthIdpConfigs/oidc.provider'.format( + PROVIDER_MGT_URL_PREFIX) + def test_get_saml_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) recorder = _instrument_provider_mgt(client, 200, SAML_PROVIDER_CONFIG_RESPONSE) @@ -765,7 +791,7 @@ def test_update_saml_provider_config(self, tenant_mgt_app): def test_delete_saml_provider_config(self, tenant_mgt_app): client = tenant_mgt.auth_for_tenant('tenant-id', app=tenant_mgt_app) - recorder = _instrument_provider_mgt(client, 200, SAML_PROVIDER_CONFIG_RESPONSE) + recorder = _instrument_provider_mgt(client, 200, '{}') client.delete_saml_provider_config('saml.provider') @@ -822,6 +848,14 @@ def _assert_request( body = json.loads(req.body.decode()) assert body == want_body + def _assert_oidc_provider_config(self, provider_config, want_id='oidc.provider'): + assert isinstance(provider_config, auth.OIDCProviderConfig) + assert provider_config.provider_id == want_id + assert provider_config.display_name == 'oidcProviderName' + assert provider_config.enabled is True + assert provider_config.client_id == 'CLIENT_ID' + assert provider_config.issuer == 'https://oidc.com/issuer' + def _assert_saml_provider_config(self, provider_config, want_id='saml.provider'): assert isinstance(provider_config, auth.SAMLProviderConfig) assert provider_config.provider_id == want_id From 641db390c514b669340d721d262b99d89952ea80 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 6 Apr 2020 13:23:41 -0700 Subject: [PATCH 2/2] Added newline to eof --- tests/data/oidc_provider_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/oidc_provider_config.json b/tests/data/oidc_provider_config.json index 2c021424c..89cf3eacf 100644 --- a/tests/data/oidc_provider_config.json +++ b/tests/data/oidc_provider_config.json @@ -4,4 +4,4 @@ "issuer": "https://oidc.com/issuer", "displayName": "oidcProviderName", "enabled": true -} \ No newline at end of file +}