Skip to content

feat(auth): Added OIDCProviderConfig type and get/delete APIs #442

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

Merged
merged 2 commits into from
Apr 7, 2020
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
29 changes: 29 additions & 0 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
37 changes: 36 additions & 1 deletion firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return the status 'success/failed' here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return of these delete methods are void.


def get_saml_provider_config(self, provider_id):
_validate_saml_provider_id(provider_id)
body = self._make_request('get', '/inboundSamlConfigs/{0}'.format(provider_id))
Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not check for empty strings though, will it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The immediate next check (if not provider_id.startswith('oidc.'):) will cover empty strings.

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(
Expand Down
36 changes: 36 additions & 0 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions tests/data/oidc_provider_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name":"projects/mock-project-id/oauthIdpConfigs/oidc.provider",
"clientId": "CLIENT_ID",
"issuer": "https://oidc.com/issuer",
"displayName": "oidcProviderName",
"enabled": true
}
59 changes: 59 additions & 0 deletions tests/test_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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 = {
Expand Down
36 changes: 35 additions & 1 deletion tests/test_tenant_mgt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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
Expand Down