diff --git a/firebase_admin/_auth_client.py b/firebase_admin/_auth_client.py index 0265197d9..eaf491f32 100644 --- a/firebase_admin/_auth_client.py +++ b/firebase_admin/_auth_client.py @@ -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 @@ -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. @@ -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: @@ -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. @@ -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. diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 5126c862c..31511f3c5 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -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. @@ -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 = { @@ -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 = {} @@ -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.') diff --git a/firebase_admin/auth.py b/firebase_admin/auth.py index cbaaf6c01..6902a322f 100644 --- a/firebase_admin/auth.py +++ b/firebase_admin/auth.py @@ -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 @@ -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. @@ -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: @@ -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. @@ -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): diff --git a/integration/test_auth.py b/integration/test_auth.py index d2d3e8577..1009816eb 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -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): @@ -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): @@ -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) @@ -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(): diff --git a/tests/test_auth_providers.py b/tests/test_auth_providers.py index 0947c77ae..b67a8eb96 100644 --- a/tests/test_auth_providers.py +++ b/tests/test_auth_providers.py @@ -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']) @@ -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) @@ -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) @@ -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) @@ -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'} @@ -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()) @@ -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):