From 709c320426ecb93e55f6365646e9f6ffd539a2ed Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 4 Dec 2024 17:33:01 +0800 Subject: [PATCH] refactor(console,core,schemas): allow SAML application to use IdP-initiated SSO (#6849) * refactor(console,core,schemas): allow SAML application to use IdP-initiated SSO allow SAML application to use IdP-initiated * fix(core): fix ut fix ut --- .../IdpInitiatedAuth/ConfigForm.tsx | 17 +---------- .../IdpInitiatedAuth/index.tsx | 17 +++++++++-- .../IdpInitiatedAuth/utils.ts | 3 +- .../core/src/libraries/sso-connector.test.ts | 19 +++++++----- packages/core/src/libraries/sso-connector.ts | 22 +++++++++----- .../en/translation/admin-console/guide.ts | 1 + ...dp-initiated-sso-application-allow-list.ts | 30 +++++++++++++++++++ ...o_connector_idp_initiated_auth_configs.sql | 2 +- 8 files changed, 77 insertions(+), 34 deletions(-) create mode 100644 packages/schemas/alterations/next-1733212543-add-saml-application-type-to-idp-initiated-sso-application-allow-list.ts diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx index 3122626ee..5ed0c7307 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/ConfigForm.tsx @@ -42,7 +42,7 @@ type FormProps = { function ConfigForm({ ssoConnector, - applications: allApplications, + applications, idpInitiatedAuthConfig, mutateIdpInitiatedConfig, }: FormProps) { @@ -50,21 +50,6 @@ function ConfigForm({ const { getTo } = useTenantPathname(); const api = useApi(); - /** - * See definition of `applicationsSearchUrl`, there is only non-third party SPA/Traditional applications here, and SAML applications are always third party secured by DB schema, we need to manually exclude other application types here to make TypeScript happy. - */ - const applications = useMemo( - () => - allApplications.filter( - ( - application - ): application is Omit & { - type: Exclude; - } => application.type !== ApplicationType.SAML - ), - [allApplications] - ); - const { control, register, diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx index 893efc675..f2646a9ee 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/index.tsx @@ -1,4 +1,8 @@ -import { type Application, type SsoConnectorWithProviderConfig } from '@logto/schemas'; +import { + ApplicationType, + type Application, + type SsoConnectorWithProviderConfig, +} from '@logto/schemas'; import { useMemo } from 'react'; import useSWR from 'swr'; @@ -31,6 +35,15 @@ function IdpInitiatedAuth({ ssoConnector }: Props) { [applicationError, applications, idpInitiatedAuthConfig, idpInitiatedAuthConfigError] ); + // Filter out non-SAML third-party applications + const filteredApplications = useMemo( + () => + applications?.filter( + ({ type, isThirdParty }) => !isThirdParty || type === ApplicationType.SAML + ), + [applications] + ); + if (isLoading) { return ( diff --git a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts index 6974193a7..d943dc95d 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts +++ b/packages/console/src/pages/EnterpriseSsoDetails/IdpInitiatedAuth/utils.ts @@ -12,7 +12,8 @@ import { toast } from 'react-hot-toast'; const applicationsSearchParams = new URLSearchParams([ ['types', ApplicationType.Traditional], ['types', ApplicationType.SPA], - ['isThirdParty', 'false'], + ['types', ApplicationType.SAML], + // TODO: for now we allow all third-party applications here, including SAML and OIDC ]); export const applicationsSearchUrl = `api/applications?${applicationsSearchParams.toString()}`; diff --git a/packages/core/src/libraries/sso-connector.test.ts b/packages/core/src/libraries/sso-connector.test.ts index 9136e8633..e7dc25cd1 100644 --- a/packages/core/src/libraries/sso-connector.test.ts +++ b/packages/core/src/libraries/sso-connector.test.ts @@ -13,7 +13,6 @@ const findAllSsoConnectors = jest.fn(); const getConnectorById = jest.fn(); const findApplicationById = jest.fn(); const insertIdpInitiatedAuthConfig = jest.fn(); -const updateIdpInitiatedAuthConfig = jest.fn(); const queries = new MockQueries({ ssoConnectors: { @@ -132,8 +131,10 @@ describe('SsoConnectorLibrary', () => { autoSendAuthorizationRequest: true, }) ).rejects.toMatchError( - new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { - type: ApplicationType.Traditional, + new RequestError({ + code: 'single_sign_on.idp_initiated_authentication_invalid_application_type', + type: `${ApplicationType.Traditional}, ${ApplicationType.SAML}`, + statue: 400, }) ); @@ -154,8 +155,10 @@ describe('SsoConnectorLibrary', () => { clientIdpInitiatedAuthCallbackUri: 'https://callback.com', }) ).rejects.toMatchError( - new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { - type: `${ApplicationType.Traditional}, ${ApplicationType.SPA}`, + new RequestError({ + code: 'single_sign_on.idp_initiated_authentication_invalid_application_type', + type: `${ApplicationType.Traditional}, ${ApplicationType.SPA}, ${ApplicationType.SAML}`, + status: 400, }) ); @@ -176,8 +179,10 @@ describe('SsoConnectorLibrary', () => { autoSendAuthorizationRequest: true, }) ).rejects.toMatchError( - new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { - type: ApplicationType.Traditional, + new RequestError({ + code: 'single_sign_on.idp_initiated_authentication_invalid_application_type', + type: `${ApplicationType.Traditional}, ${ApplicationType.SAML}`, + status: 400, }) ); diff --git a/packages/core/src/libraries/sso-connector.ts b/packages/core/src/libraries/sso-connector.ts index c2d640afb..e74a608bd 100644 --- a/packages/core/src/libraries/sso-connector.ts +++ b/packages/core/src/libraries/sso-connector.ts @@ -85,11 +85,14 @@ export const createSsoConnectorLibrary = (queries: Queries) => { // Authorization request initiated by Logto server if (autoSendAuthorizationRequest) { - // Only first-party traditional web applications are allowed + // Only first-party traditional web applications or SAML applications are allowed assertThat( - application.type === ApplicationType.Traditional && !application.isThirdParty, - new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { - type: ApplicationType.Traditional, + (application.type === ApplicationType.Traditional && !application.isThirdParty) || + application.type === ApplicationType.SAML, + new RequestError({ + code: 'single_sign_on.idp_initiated_authentication_invalid_application_type', + type: `${ApplicationType.Traditional}, ${ApplicationType.SAML}`, + status: 400, }) ); @@ -100,11 +103,16 @@ export const createSsoConnectorLibrary = (queries: Queries) => { ); } else { // Authorization request initiated by the client + + // Only first-party traditional web applications, SPAs, or SAML applications are allowed assertThat( (application.type === ApplicationType.Traditional && !application.isThirdParty) || - application.type === ApplicationType.SPA, - new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { - type: `${ApplicationType.Traditional}, ${ApplicationType.SPA}`, + application.type === ApplicationType.SPA || + application.type === ApplicationType.SAML, + new RequestError({ + code: 'single_sign_on.idp_initiated_authentication_invalid_application_type', + type: `${ApplicationType.Traditional}, ${ApplicationType.SPA}, ${ApplicationType.SAML}`, + status: 400, }) ); diff --git a/packages/phrases/src/locales/en/translation/admin-console/guide.ts b/packages/phrases/src/locales/en/translation/admin-console/guide.ts index 4c45aae6d..284561212 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/guide.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/guide.ts @@ -9,6 +9,7 @@ const guide = { MachineToMachine: 'Machine-to-machine', Protected: 'Non-SDK Integration', ThirdParty: 'Third-party app', + SAML: 'SAML', }, filter: { title: 'Filter framework', diff --git a/packages/schemas/alterations/next-1733212543-add-saml-application-type-to-idp-initiated-sso-application-allow-list.ts b/packages/schemas/alterations/next-1733212543-add-saml-application-type-to-idp-initiated-sso-application-allow-list.ts new file mode 100644 index 000000000..8f0908f83 --- /dev/null +++ b/packages/schemas/alterations/next-1733212543-add-saml-application-type-to-idp-initiated-sso-application-allow-list.ts @@ -0,0 +1,30 @@ +import { sql } from '@silverhand/slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const alteration: AlterationScript = { + up: async (pool) => { + await pool.query(sql` + alter table sso_connector_idp_initiated_auth_configs + drop constraint application_type;`); + + await pool.query(sql` + alter table sso_connector_idp_initiated_auth_configs + add constraint application_type + check (check_application_type(default_application_id, 'Traditional', 'SPA', 'SAML')); + `); + }, + down: async (pool) => { + await pool.query(sql` + alter table sso_connector_idp_initiated_auth_configs + drop constraint application_type;`); + + await pool.query(sql` + alter table sso_connector_idp_initiated_auth_configs + add constraint application_type + check (check_application_type(default_application_id, 'Traditional', 'SPA')); + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/tables/sso_connector_idp_initiated_auth_configs.sql b/packages/schemas/tables/sso_connector_idp_initiated_auth_configs.sql index 33897900b..4ced81ae1 100644 --- a/packages/schemas/tables/sso_connector_idp_initiated_auth_configs.sql +++ b/packages/schemas/tables/sso_connector_idp_initiated_auth_configs.sql @@ -20,5 +20,5 @@ create table sso_connector_idp_initiated_auth_configs ( primary key (tenant_id, connector_id), /** Insure the application type is Traditional or SPA. */ constraint application_type - check (check_application_type(default_application_id, 'Traditional', 'SPA')) + check (check_application_type(default_application_id, 'Traditional', 'SPA', 'SAML')) );