0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

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
This commit is contained in:
simeng-li 2024-12-04 17:33:01 +08:00 committed by GitHub
parent 239b81e31a
commit 709c320426
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 77 additions and 34 deletions

View file

@ -42,7 +42,7 @@ type FormProps = {
function ConfigForm({ function ConfigForm({
ssoConnector, ssoConnector,
applications: allApplications, applications,
idpInitiatedAuthConfig, idpInitiatedAuthConfig,
mutateIdpInitiatedConfig, mutateIdpInitiatedConfig,
}: FormProps) { }: FormProps) {
@ -50,21 +50,6 @@ function ConfigForm({
const { getTo } = useTenantPathname(); const { getTo } = useTenantPathname();
const api = useApi(); 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<Application, 'type'> & {
type: Exclude<ApplicationType, ApplicationType.SAML>;
} => application.type !== ApplicationType.SAML
),
[allApplications]
);
const { const {
control, control,
register, register,

View file

@ -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 { useMemo } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
@ -31,6 +35,15 @@ function IdpInitiatedAuth({ ssoConnector }: Props) {
[applicationError, applications, idpInitiatedAuthConfig, idpInitiatedAuthConfigError] [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) { if (isLoading) {
return ( return (
<FormCard <FormCard
@ -45,7 +58,7 @@ function IdpInitiatedAuth({ ssoConnector }: Props) {
return ( return (
<ConfigForm <ConfigForm
ssoConnector={ssoConnector} ssoConnector={ssoConnector}
applications={applications ?? []} applications={filteredApplications ?? []}
idpInitiatedAuthConfig={idpInitiatedAuthConfig} idpInitiatedAuthConfig={idpInitiatedAuthConfig}
mutateIdpInitiatedConfig={mutate} mutateIdpInitiatedConfig={mutate}
/> />

View file

@ -12,7 +12,8 @@ import { toast } from 'react-hot-toast';
const applicationsSearchParams = new URLSearchParams([ const applicationsSearchParams = new URLSearchParams([
['types', ApplicationType.Traditional], ['types', ApplicationType.Traditional],
['types', ApplicationType.SPA], ['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()}`; export const applicationsSearchUrl = `api/applications?${applicationsSearchParams.toString()}`;

View file

@ -13,7 +13,6 @@ const findAllSsoConnectors = jest.fn();
const getConnectorById = jest.fn(); const getConnectorById = jest.fn();
const findApplicationById = jest.fn(); const findApplicationById = jest.fn();
const insertIdpInitiatedAuthConfig = jest.fn(); const insertIdpInitiatedAuthConfig = jest.fn();
const updateIdpInitiatedAuthConfig = jest.fn();
const queries = new MockQueries({ const queries = new MockQueries({
ssoConnectors: { ssoConnectors: {
@ -132,8 +131,10 @@ describe('SsoConnectorLibrary', () => {
autoSendAuthorizationRequest: true, autoSendAuthorizationRequest: true,
}) })
).rejects.toMatchError( ).rejects.toMatchError(
new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { new RequestError({
type: ApplicationType.Traditional, 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', clientIdpInitiatedAuthCallbackUri: 'https://callback.com',
}) })
).rejects.toMatchError( ).rejects.toMatchError(
new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { new RequestError({
type: `${ApplicationType.Traditional}, ${ApplicationType.SPA}`, 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, autoSendAuthorizationRequest: true,
}) })
).rejects.toMatchError( ).rejects.toMatchError(
new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { new RequestError({
type: ApplicationType.Traditional, code: 'single_sign_on.idp_initiated_authentication_invalid_application_type',
type: `${ApplicationType.Traditional}, ${ApplicationType.SAML}`,
status: 400,
}) })
); );

View file

@ -85,11 +85,14 @@ export const createSsoConnectorLibrary = (queries: Queries) => {
// Authorization request initiated by Logto server // Authorization request initiated by Logto server
if (autoSendAuthorizationRequest) { if (autoSendAuthorizationRequest) {
// Only first-party traditional web applications are allowed // Only first-party traditional web applications or SAML applications are allowed
assertThat( assertThat(
application.type === ApplicationType.Traditional && !application.isThirdParty, (application.type === ApplicationType.Traditional && !application.isThirdParty) ||
new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { application.type === ApplicationType.SAML,
type: ApplicationType.Traditional, 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 { } else {
// Authorization request initiated by the client // Authorization request initiated by the client
// Only first-party traditional web applications, SPAs, or SAML applications are allowed
assertThat( assertThat(
(application.type === ApplicationType.Traditional && !application.isThirdParty) || (application.type === ApplicationType.Traditional && !application.isThirdParty) ||
application.type === ApplicationType.SPA, application.type === ApplicationType.SPA ||
new RequestError('single_sign_on.idp_initiated_authentication_invalid_application_type', { application.type === ApplicationType.SAML,
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,
}) })
); );

View file

@ -9,6 +9,7 @@ const guide = {
MachineToMachine: 'Machine-to-machine', MachineToMachine: 'Machine-to-machine',
Protected: 'Non-SDK Integration', Protected: 'Non-SDK Integration',
ThirdParty: 'Third-party app', ThirdParty: 'Third-party app',
SAML: 'SAML',
}, },
filter: { filter: {
title: 'Filter framework', title: 'Filter framework',

View file

@ -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;

View file

@ -20,5 +20,5 @@ create table sso_connector_idp_initiated_auth_configs (
primary key (tenant_id, connector_id), primary key (tenant_id, connector_id),
/** Insure the application type is Traditional or SPA. */ /** Insure the application type is Traditional or SPA. */
constraint application_type constraint application_type
check (check_application_type(default_application_id, 'Traditional', 'SPA')) check (check_application_type(default_application_id, 'Traditional', 'SPA', 'SAML'))
); );