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:
parent
239b81e31a
commit
709c320426
8 changed files with 77 additions and 34 deletions
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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()}`;
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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;
|
|
@ -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'))
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue