Skip to content

Commit

Permalink
feat(auth): enables OIDC auth code flow (#549)
Browse files Browse the repository at this point in the history
Provides an option for developers to specify the OAuth response type for their OIDC provider (either one of these can be set:):
- id_token
- code (if set, must also set the client secret)

RELEASE NOTES: Added support for configuring the authorization code flow for OIDC providers.
  • Loading branch information
ScruffyProdigy authored Dec 14, 2021
1 parent 02596dc commit 008b1d8
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 16 deletions.
32 changes: 28 additions & 4 deletions firebase_admin/_auth_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,8 @@ def get_oidc_provider_config(self, provider_id):
return self._provider_manager.get_oidc_provider_config(provider_id)

def create_oidc_provider_config(
self, provider_id, client_id, issuer, display_name=None, enabled=None):
self, provider_id, client_id, issuer, display_name=None, enabled=None,
client_secret=None, id_token_response_type=None, code_response_type=None):
"""Creates a new OIDC provider config from the given parameters.
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
Expand All @@ -528,6 +529,16 @@ def create_oidc_provider_config(
This name is also used as the provider label in the Cloud Console.
enabled: A boolean indicating whether the provider configuration is enabled or disabled
(optional). A user cannot sign in using a disabled provider.
client_secret: A string which sets the client secret for the new provider.
This is required for the code flow.
code_response_type: A boolean which sets whether to enable the code response flow for
the new provider. By default, this is not enabled if no response type is
specified. A client secret must be set for this response type.
Having both the code and ID token response flows is currently not supported.
id_token_response_type: A boolean which sets whether to enable the ID token response
flow for the new provider. By default, this is enabled if no response type is
specified.
Having both the code and ID token response flows is currently not supported.
Returns:
OIDCProviderConfig: The newly created OIDC provider config instance.
Expand All @@ -538,10 +549,12 @@ def create_oidc_provider_config(
"""
return self._provider_manager.create_oidc_provider_config(
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
enabled=enabled)
enabled=enabled, client_secret=client_secret,
id_token_response_type=id_token_response_type, code_response_type=code_response_type)

def update_oidc_provider_config(
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None,
client_secret=None, id_token_response_type=None, code_response_type=None):
"""Updates an existing OIDC provider config with the given parameters.
Args:
Expand All @@ -552,6 +565,16 @@ def update_oidc_provider_config(
Pass ``auth.DELETE_ATTRIBUTE`` to delete the current display name.
enabled: A boolean indicating whether the provider configuration is enabled or disabled
(optional).
client_secret: A string which sets the client secret for the new provider.
This is required for the code flow.
code_response_type: A boolean which sets whether to enable the code response flow for
the new provider. By default, this is not enabled if no response type is specified.
A client secret must be set for this response type.
Having both the code and ID token response flows is currently not supported.
id_token_response_type: A boolean which sets whether to enable the ID token response
flow for the new provider. By default, this is enabled if no response type is
specified.
Having both the code and ID token response flows is currently not supported.
Returns:
OIDCProviderConfig: The updated OIDC provider config instance.
Expand All @@ -562,7 +585,8 @@ def update_oidc_provider_config(
"""
return self._provider_manager.update_oidc_provider_config(
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
enabled=enabled)
enabled=enabled, client_secret=client_secret,
id_token_response_type=id_token_response_type, code_response_type=code_response_type)

def delete_oidc_provider_config(self, provider_id):
"""Deletes the ``OIDCProviderConfig`` with the given ID.
Expand Down
47 changes: 45 additions & 2 deletions firebase_admin/_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ def issuer(self):
def client_id(self):
return self._data['clientId']

@property
def client_secret(self):
return self._data.get('clientSecret')

@property
def id_token_response_type(self):
return self._data.get('responseType', {}).get('idToken', False)

@property
def code_response_type(self):
return self._data.get('responseType', {}).get('code', False)


class SAMLProviderConfig(ProviderConfig):
"""Represents he SAML auth provider configuration.
Expand Down Expand Up @@ -179,7 +191,8 @@ def get_oidc_provider_config(self, provider_id):
return OIDCProviderConfig(body)

def create_oidc_provider_config(
self, provider_id, client_id, issuer, display_name=None, enabled=None):
self, provider_id, client_id, issuer, display_name=None, enabled=None,
client_secret=None, id_token_response_type=None, code_response_type=None):
"""Creates a new OIDC provider config from the given parameters."""
_validate_oidc_provider_id(provider_id)
req = {
Expand All @@ -191,12 +204,28 @@ def create_oidc_provider_config(
if enabled is not None:
req['enabled'] = _auth_utils.validate_boolean(enabled, 'enabled')

response_type = {}
if id_token_response_type is False and code_response_type is False:
raise ValueError('At least one response type must be returned.')
if id_token_response_type is not None:
response_type['idToken'] = _auth_utils.validate_boolean(
id_token_response_type, 'id_token_response_type')
if code_response_type is not None:
response_type['code'] = _auth_utils.validate_boolean(
code_response_type, 'code_response_type')
if code_response_type:
req['clientSecret'] = _validate_non_empty_string(client_secret, 'client_secret')
if response_type:
req['responseType'] = response_type

params = 'oauthIdpConfigId={0}'.format(provider_id)
body = self._make_request('post', '/oauthIdpConfigs', json=req, params=params)
return OIDCProviderConfig(body)

def update_oidc_provider_config(
self, provider_id, client_id=None, issuer=None, display_name=None, enabled=None):
self, provider_id, client_id=None, issuer=None, display_name=None,
enabled=None, client_secret=None, id_token_response_type=None,
code_response_type=None):
"""Updates an existing OIDC provider config with the given parameters."""
_validate_oidc_provider_id(provider_id)
req = {}
Expand All @@ -212,6 +241,20 @@ def update_oidc_provider_config(
if issuer:
req['issuer'] = _validate_url(issuer, 'issuer')

response_type = {}
if id_token_response_type is False and code_response_type is False:
raise ValueError('At least one response type must be returned.')
if id_token_response_type is not None:
response_type['idToken'] = _auth_utils.validate_boolean(
id_token_response_type, 'id_token_response_type')
if code_response_type is not None:
response_type['code'] = _auth_utils.validate_boolean(
code_response_type, 'code_response_type')
if code_response_type:
req['clientSecret'] = _validate_non_empty_string(client_secret, 'client_secret')
if response_type:
req['responseType'] = response_type

if not req:
raise ValueError('At least one parameter must be specified for update.')

Expand Down
30 changes: 26 additions & 4 deletions firebase_admin/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,8 @@ def get_oidc_provider_config(provider_id, app=None):
return client.get_oidc_provider_config(provider_id)

def create_oidc_provider_config(
provider_id, client_id, issuer, display_name=None, enabled=None, app=None):
provider_id, client_id, issuer, display_name=None, enabled=None, client_secret=None,
id_token_response_type=None, code_response_type=None, app=None):
"""Creates a new OIDC provider config from the given parameters.
OIDC provider support requires Google Cloud's Identity Platform (GCIP). To learn more about
Expand All @@ -671,6 +672,15 @@ def create_oidc_provider_config(
enabled: A boolean indicating whether the provider configuration is enabled or disabled
(optional). A user cannot sign in using a disabled provider.
app: An App instance (optional).
client_secret: A string which sets the client secret for the new provider.
This is required for the code flow.
code_response_type: A boolean which sets whether to enable the code response flow for the
new provider. By default, this is not enabled if no response type is specified.
A client secret must be set for this response type.
Having both the code and ID token response flows is currently not supported.
id_token_response_type: A boolean which sets whether to enable the ID token response flow
for the new provider. By default, this is enabled if no response type is specified.
Having both the code and ID token response flows is currently not supported.
Returns:
OIDCProviderConfig: The newly created OIDC provider config instance.
Expand All @@ -682,11 +692,13 @@ def create_oidc_provider_config(
client = _get_client(app)
return client.create_oidc_provider_config(
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
enabled=enabled)
enabled=enabled, client_secret=client_secret, id_token_response_type=id_token_response_type,
code_response_type=code_response_type)


def update_oidc_provider_config(
provider_id, client_id=None, issuer=None, display_name=None, enabled=None, app=None):
provider_id, client_id=None, issuer=None, display_name=None, enabled=None,
client_secret=None, id_token_response_type=None, code_response_type=None, app=None):
"""Updates an existing OIDC provider config with the given parameters.
Args:
Expand All @@ -698,6 +710,15 @@ def update_oidc_provider_config(
enabled: A boolean indicating whether the provider configuration is enabled or disabled
(optional).
app: An App instance (optional).
client_secret: A string which sets the client secret for the new provider.
This is required for the code flow.
code_response_type: A boolean which sets whether to enable the code response flow for the
new provider. By default, this is not enabled if no response type is specified.
A client secret must be set for this response type.
Having both the code and ID token response flows is currently not supported.
id_token_response_type: A boolean which sets whether to enable the ID token response flow
for the new provider. By default, this is enabled if no response type is specified.
Having both the code and ID token response flows is currently not supported.
Returns:
OIDCProviderConfig: The updated OIDC provider config instance.
Expand All @@ -709,7 +730,8 @@ def update_oidc_provider_config(
client = _get_client(app)
return client.update_oidc_provider_config(
provider_id, client_id=client_id, issuer=issuer, display_name=display_name,
enabled=enabled)
enabled=enabled, client_secret=client_secret, id_token_response_type=id_token_response_type,
code_response_type=code_response_type)


def delete_oidc_provider_config(provider_id, app=None):
Expand Down
18 changes: 16 additions & 2 deletions integration/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,9 @@ def test_create_oidc_provider_config(oidc_provider):
assert oidc_provider.issuer == 'https://oidc.com/issuer'
assert oidc_provider.display_name == 'OIDC_DISPLAY_NAME'
assert oidc_provider.enabled is True
assert oidc_provider.response_type.id_token is True
assert oidc_provider.response_type.code is False
assert oidc_provider.client_secret is None


def test_get_oidc_provider_config(oidc_provider):
Expand All @@ -746,6 +749,9 @@ def test_get_oidc_provider_config(oidc_provider):
assert provider_config.issuer == 'https://oidc.com/issuer'
assert provider_config.display_name == 'OIDC_DISPLAY_NAME'
assert provider_config.enabled is True
assert provider_config.response_type.id_token is True
assert provider_config.response_type.code is False
assert provider_config.client_secret is None


def test_list_oidc_provider_configs(oidc_provider):
Expand All @@ -767,11 +773,17 @@ def test_update_oidc_provider_config():
client_id='UPDATED_OIDC_CLIENT_ID',
issuer='https://oidc.com/updated_issuer',
display_name='UPDATED_OIDC_DISPLAY_NAME',
enabled=False)
enabled=False,
client_secret='CLIENT_SECRET',
id_token_response_type=False,
code_response_type=True)
assert provider_config.client_id == 'UPDATED_OIDC_CLIENT_ID'
assert provider_config.issuer == 'https://oidc.com/updated_issuer'
assert provider_config.display_name == 'UPDATED_OIDC_DISPLAY_NAME'
assert provider_config.enabled is False
assert provider_config.response_type.id_token is False
assert provider_config.response_type.code is True
assert provider_config.client_secret == 'CLIENT_SECRET'
finally:
auth.delete_oidc_provider_config(provider_config.provider_id)

Expand Down Expand Up @@ -863,7 +875,9 @@ def _create_oidc_provider_config():
client_id='OIDC_CLIENT_ID',
issuer='https://oidc.com/issuer',
display_name='OIDC_DISPLAY_NAME',
enabled=True)
enabled=True,
id_token_response_type=True,
code_response_type=False)


def _create_saml_provider_config():
Expand Down
39 changes: 35 additions & 4 deletions tests/test_auth_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,21 @@ class TestOIDCProviderConfig:
'issuer': 'https://oidc.com/issuer',
'display_name': 'oidcProviderName',
'enabled': True,
'id_token_response_type': True,
'code_response_type': True,
'client_secret': 'CLIENT_SECRET',
}

OIDC_CONFIG_REQUEST = {
'displayName': 'oidcProviderName',
'enabled': True,
'clientId': 'CLIENT_ID',
'clientSecret': 'CLIENT_SECRET',
'issuer': 'https://oidc.com/issuer',
'responseType': {
'code': True,
'idToken': True,
},
}

@pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider'])
Expand All @@ -112,6 +120,11 @@ def test_get(self, user_mgt_app):
{'issuer': None}, {'issuer': ''}, {'issuer': 'not a url'},
{'display_name': True},
{'enabled': 'true'},
{'id_token_response_type': 'true'}, {'code_response_type': 'true'},
{'code_response_type': True, 'client_secret': ''},
{'code_response_type': True, 'client_secret': True},
{'code_response_type': True, 'client_secret': None},
{'code_response_type': False, 'id_token_response_type': False},
])
def test_create_invalid_args(self, user_mgt_app, invalid_opts):
options = dict(self.VALID_CREATE_OPTIONS)
Expand Down Expand Up @@ -139,9 +152,14 @@ def test_create_minimal(self, user_mgt_app):
options = dict(self.VALID_CREATE_OPTIONS)
del options['display_name']
del options['enabled']
del options['client_secret']
del options['id_token_response_type']
del options['code_response_type']
want = dict(self.OIDC_CONFIG_REQUEST)
del want['displayName']
del want['enabled']
del want['clientSecret']
del want['responseType']

provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app)

Expand All @@ -159,9 +177,15 @@ def test_create_empty_values(self, user_mgt_app):
options = dict(self.VALID_CREATE_OPTIONS)
options['display_name'] = ''
options['enabled'] = False
options['code_response_type'] = False
want = dict(self.OIDC_CONFIG_REQUEST)
want['displayName'] = ''
want['enabled'] = False
want['responseType'] = {
'code': False,
'idToken': True,
}
del want['clientSecret']

provider_config = auth.create_oidc_provider_config(**options, app=user_mgt_app)

Expand All @@ -181,6 +205,11 @@ def test_create_empty_values(self, user_mgt_app):
{'issuer': ''}, {'issuer': 'not a url'},
{'display_name': True},
{'enabled': 'true'},
{'id_token_response_type': 'true'}, {'code_response_type': 'true'},
{'code_response_type': True, 'client_secret': ''},
{'code_response_type': True, 'client_secret': True},
{'code_response_type': True, 'client_secret': None},
{'code_response_type': False, 'id_token_response_type': False},
])
def test_update_invalid_args(self, user_mgt_app, invalid_opts):
options = {'provider_id': 'oidc.provider'}
Expand All @@ -198,7 +227,8 @@ def test_update(self, user_mgt_app):
assert len(recorder) == 1
req = recorder[0]
assert req.method == 'PATCH'
mask = ['clientId', 'displayName', 'enabled', 'issuer']
mask = ['clientId', 'clientSecret', 'displayName', 'enabled', 'issuer',
'responseType.code', 'responseType.idToken']
assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format(
USER_MGT_URLS['PREFIX'], ','.join(mask))
got = json.loads(req.body.decode())
Expand All @@ -223,17 +253,18 @@ def test_update_empty_values(self, user_mgt_app):
recorder = _instrument_provider_mgt(user_mgt_app, 200, OIDC_PROVIDER_CONFIG_RESPONSE)

provider_config = auth.update_oidc_provider_config(
'oidc.provider', display_name=auth.DELETE_ATTRIBUTE, enabled=False, app=user_mgt_app)
'oidc.provider', display_name=auth.DELETE_ATTRIBUTE, enabled=False,
id_token_response_type=False, app=user_mgt_app)

self._assert_provider_config(provider_config)
assert len(recorder) == 1
req = recorder[0]
assert req.method == 'PATCH'
mask = ['displayName', 'enabled']
mask = ['displayName', 'enabled', 'responseType.idToken']
assert req.url == '{0}/oauthIdpConfigs/oidc.provider?updateMask={1}'.format(
USER_MGT_URLS['PREFIX'], ','.join(mask))
got = json.loads(req.body.decode())
assert got == {'displayName': None, 'enabled': False}
assert got == {'displayName': None, 'enabled': False, 'responseType': {'idToken': False}}

@pytest.mark.parametrize('provider_id', INVALID_PROVIDER_IDS + ['saml.provider'])
def test_delete_invalid_provider_id(self, user_mgt_app, provider_id):
Expand Down

0 comments on commit 008b1d8

Please sign in to comment.