mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(core,schemas): add sso idp initiated auth config api (#6660)
* feat(core,schemas): add sso idp initiated auth config api add sso connector idp initiated auth config management api * fix(core): align the naming convention align the naming convention
This commit is contained in:
parent
22566e375c
commit
14a07dcead
12 changed files with 426 additions and 3 deletions
|
@ -44,6 +44,7 @@ const ssoConnectorLibrary = {
|
||||||
getSsoConnectors: jest.fn(),
|
getSsoConnectors: jest.fn(),
|
||||||
getSsoConnectorById: jest.fn(),
|
getSsoConnectorById: jest.fn(),
|
||||||
getAvailableSsoConnectors: jest.fn(),
|
getAvailableSsoConnectors: jest.fn(),
|
||||||
|
createSsoConnectorIdpInitiatedAuthConfig: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { MockQueries } = await import('#src/test-utils/tenant.js');
|
const { MockQueries } = await import('#src/test-utils/tenant.js');
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { type SupportedSsoConnector } from '@logto/schemas';
|
import {
|
||||||
|
ApplicationType,
|
||||||
|
type CreateSsoConnectorIdpInitiatedAuthConfig,
|
||||||
|
type SupportedSsoConnector,
|
||||||
|
} from '@logto/schemas';
|
||||||
import { assert } from '@silverhand/essentials';
|
import { assert } from '@silverhand/essentials';
|
||||||
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
@ -6,10 +10,13 @@ import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||||
import { isSupportedSsoConnector } from '#src/sso/utils.js';
|
import { isSupportedSsoConnector } from '#src/sso/utils.js';
|
||||||
import type Queries from '#src/tenants/Queries.js';
|
import type Queries from '#src/tenants/Queries.js';
|
||||||
|
|
||||||
|
import assertThat from '../utils/assert-that.js';
|
||||||
|
import { type OmitAutoSetFields } from '../utils/sql.js';
|
||||||
|
|
||||||
export type SsoConnectorLibrary = ReturnType<typeof createSsoConnectorLibrary>;
|
export type SsoConnectorLibrary = ReturnType<typeof createSsoConnectorLibrary>;
|
||||||
|
|
||||||
export const createSsoConnectorLibrary = (queries: Queries) => {
|
export const createSsoConnectorLibrary = (queries: Queries) => {
|
||||||
const { ssoConnectors } = queries;
|
const { ssoConnectors, applications } = queries;
|
||||||
|
|
||||||
const getSsoConnectors = async (
|
const getSsoConnectors = async (
|
||||||
limit?: number,
|
limit?: number,
|
||||||
|
@ -52,9 +59,32 @@ export const createSsoConnectorLibrary = (queries: Queries) => {
|
||||||
return connector;
|
return connector;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new IdP-initiated authentication configuration for a SSO connector.
|
||||||
|
*
|
||||||
|
* @throws {RequestError} Throws a 404 error if the application is not found
|
||||||
|
* @throws {RequestError} Throws a 400 error if the application is not a first-party traditional web application
|
||||||
|
*/
|
||||||
|
const createSsoConnectorIdpInitiatedAuthConfig = async (
|
||||||
|
data: OmitAutoSetFields<CreateSsoConnectorIdpInitiatedAuthConfig>
|
||||||
|
) => {
|
||||||
|
const { defaultApplicationId } = data;
|
||||||
|
|
||||||
|
// Throws an 404 error if the application is not found
|
||||||
|
const application = await applications.findApplicationById(defaultApplicationId);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
application.type === ApplicationType.Traditional && !application.isThirdParty,
|
||||||
|
new RequestError('connector.saml_idp_initiated_auth_invalid_application_type')
|
||||||
|
);
|
||||||
|
|
||||||
|
return ssoConnectors.insertIdpInitiatedAuthConfig(data);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getSsoConnectors,
|
getSsoConnectors,
|
||||||
getAvailableSsoConnectors,
|
getAvailableSsoConnectors,
|
||||||
getSsoConnectorById,
|
getSsoConnectorById,
|
||||||
|
createSsoConnectorIdpInitiatedAuthConfig,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
type CreateSsoConnector,
|
type CreateSsoConnector,
|
||||||
type SsoConnector,
|
type SsoConnector,
|
||||||
|
type SsoConnectorIdpInitiatedAuthConfig,
|
||||||
|
SsoConnectorIdpInitiatedAuthConfigs,
|
||||||
type SsoConnectorKeys,
|
type SsoConnectorKeys,
|
||||||
SsoConnectors,
|
SsoConnectors,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
@ -9,11 +11,42 @@ import { sql, type CommonQueryMethods } from '@silverhand/slonik';
|
||||||
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
import SchemaQueries from '#src/utils/SchemaQueries.js';
|
||||||
import { convertToIdentifiers } from '#src/utils/sql.js';
|
import { convertToIdentifiers } from '#src/utils/sql.js';
|
||||||
|
|
||||||
|
import { buildInsertIntoWithPool } from '../database/insert-into.js';
|
||||||
|
import { buildUpdateWhereWithPool } from '../database/update-where.js';
|
||||||
|
|
||||||
|
const {
|
||||||
|
table: ssoConnectorIdpInitiatedAuthConfigsTable,
|
||||||
|
fields: ssoConnectorIdpInitiatedAuthConfigsFields,
|
||||||
|
} = convertToIdentifiers(SsoConnectorIdpInitiatedAuthConfigs);
|
||||||
|
|
||||||
export default class SsoConnectorQueries extends SchemaQueries<
|
export default class SsoConnectorQueries extends SchemaQueries<
|
||||||
SsoConnectorKeys,
|
SsoConnectorKeys,
|
||||||
CreateSsoConnector,
|
CreateSsoConnector,
|
||||||
SsoConnector
|
SsoConnector
|
||||||
> {
|
> {
|
||||||
|
public readonly insertIdpInitiatedAuthConfig = buildInsertIntoWithPool(this.pool)(
|
||||||
|
SsoConnectorIdpInitiatedAuthConfigs,
|
||||||
|
{
|
||||||
|
returning: true,
|
||||||
|
onConflict: {
|
||||||
|
fields: [
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigsFields.connectorId,
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigsFields.tenantId,
|
||||||
|
],
|
||||||
|
setExcludedFields: [
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigsFields.defaultApplicationId,
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigsFields.redirectUri,
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigsFields.authParameters,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public readonly updateIdpInitiatedAuthConfig = buildUpdateWhereWithPool(this.pool)(
|
||||||
|
SsoConnectorIdpInitiatedAuthConfigs,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
constructor(pool: CommonQueryMethods) {
|
constructor(pool: CommonQueryMethods) {
|
||||||
super(pool, SsoConnectors);
|
super(pool, SsoConnectors);
|
||||||
}
|
}
|
||||||
|
@ -26,4 +59,11 @@ export default class SsoConnectorQueries extends SchemaQueries<
|
||||||
where ${fields.connectorName}=${connectorName}
|
where ${fields.connectorName}=${connectorName}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getIdpInitiatedAuthConfigByConnectorId(connectorId: string) {
|
||||||
|
return this.pool.maybeOne<SsoConnectorIdpInitiatedAuthConfig>(sql`
|
||||||
|
SELECT * FROM ${ssoConnectorIdpInitiatedAuthConfigsTable}
|
||||||
|
WHERE ${ssoConnectorIdpInitiatedAuthConfigsFields.connectorId}=${connectorId}
|
||||||
|
`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ const mockSsoConnectorLibrary: SsoConnectorLibrary = {
|
||||||
getAvailableSsoConnectors: getAvailableSsoConnectorsMock,
|
getAvailableSsoConnectors: getAvailableSsoConnectorsMock,
|
||||||
getSsoConnectors: jest.fn(),
|
getSsoConnectors: jest.fn(),
|
||||||
getSsoConnectorById: jest.fn(),
|
getSsoConnectorById: jest.fn(),
|
||||||
|
createSsoConnectorIdpInitiatedAuthConfig: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('verifyEmailIdentifier tests', () => {
|
describe('verifyEmailIdentifier tests', () => {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Dev feature"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/api/sso-connectors/{id}/idp-initiated-auth-config": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Get IdP initiated auth config",
|
||||||
|
"description": "Get the IdP initiated authentication config of the given SAML SSO connector.",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The IdP initiated authentication config."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "SSO connector or the IdP initiated auth config not found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"summary": "Set IdP initiated auth config",
|
||||||
|
"description": "Set IdP initiated authentication config for a given SAML SSO connector. Any existing IdP initiated auth config will be overwritten.",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"defaultApplicationId": {
|
||||||
|
"description": "The unique identifier for the application that users will sign in to using IdP initiated authentication. The application type must be `Traditional` and cannot be a third-party application."
|
||||||
|
},
|
||||||
|
"redirectUri": {
|
||||||
|
"description": "The sign-in redirect URI for the application. This URI must be registered in the application's OIDC client metadata. If not provided, Logto will use the first registered redirect URI of the application."
|
||||||
|
},
|
||||||
|
"authParameters": {
|
||||||
|
"description": "The additional parameters to be sent to the application's OIDC authorization endpoint, e.g. `resources` and `scopes`."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The updated IdP initiated auth config."
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "SSO connector or application not found."
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "The request body is invalid. The SSO connector is not a SAML connector or the application is not a Traditional web application."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
import {
|
||||||
|
SsoConnectors,
|
||||||
|
SsoConnectorIdpInitiatedAuthConfigs,
|
||||||
|
SsoProviderType,
|
||||||
|
} from '@logto/schemas';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
|
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||||
|
import { tableToPathname } from '#src/utils/SchemaRouter.js';
|
||||||
|
|
||||||
|
import assertThat from '../../utils/assert-that.js';
|
||||||
|
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
|
||||||
|
|
||||||
|
export default function ssoConnectorIdpInitiatedAuthConfigRoutes<T extends ManagementApiRouter>(
|
||||||
|
...args: RouterInitArgs<T>
|
||||||
|
) {
|
||||||
|
const [
|
||||||
|
router,
|
||||||
|
{
|
||||||
|
queries,
|
||||||
|
libraries: {
|
||||||
|
ssoConnectors: { getSsoConnectorById, createSsoConnectorIdpInitiatedAuthConfig },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] = args;
|
||||||
|
|
||||||
|
const pathPrefix = `/${tableToPathname(SsoConnectors.table)}/:id/idp-initiated-auth-config`;
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
pathPrefix,
|
||||||
|
koaGuard({
|
||||||
|
body: SsoConnectorIdpInitiatedAuthConfigs.createGuard.pick({
|
||||||
|
defaultApplicationId: true,
|
||||||
|
redirectUri: true,
|
||||||
|
authParameters: true,
|
||||||
|
}),
|
||||||
|
params: z.object({ id: z.string().min(1) }),
|
||||||
|
response: SsoConnectorIdpInitiatedAuthConfigs.guard,
|
||||||
|
status: [200, 400, 404],
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const {
|
||||||
|
body,
|
||||||
|
params: { id },
|
||||||
|
} = ctx.guard;
|
||||||
|
|
||||||
|
// Throws an 404 error if the connector is not found
|
||||||
|
const { providerName } = await getSsoConnectorById(id);
|
||||||
|
const { providerType } = ssoConnectorFactories[providerName];
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
providerType === SsoProviderType.SAML,
|
||||||
|
new RequestError('connector.saml_only_idp_initiated_auth')
|
||||||
|
);
|
||||||
|
|
||||||
|
const config = await createSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId: id,
|
||||||
|
...body,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.body = config;
|
||||||
|
ctx.status = 200;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
pathPrefix,
|
||||||
|
koaGuard({
|
||||||
|
params: z.object({ id: z.string().min(1) }),
|
||||||
|
response: SsoConnectorIdpInitiatedAuthConfigs.guard,
|
||||||
|
status: [200, 404],
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const {
|
||||||
|
params: { id },
|
||||||
|
} = ctx.guard;
|
||||||
|
|
||||||
|
const configs = await queries.ssoConnectors.getIdpInitiatedAuthConfigByConnectorId(id);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
configs,
|
||||||
|
new RequestError({
|
||||||
|
code: 'entity.not_found',
|
||||||
|
status: 404,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.body = configs;
|
||||||
|
ctx.status = 200;
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,8 +17,10 @@ import { isSupportedSsoConnector, isSupportedSsoProvider } from '#src/sso/utils.
|
||||||
import { tableToPathname } from '#src/utils/SchemaRouter.js';
|
import { tableToPathname } from '#src/utils/SchemaRouter.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
|
|
||||||
|
import { EnvSet } from '../../env-set/index.js';
|
||||||
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
|
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js';
|
||||||
|
|
||||||
|
import ssoConnectorIdpInitiatedAuthConfigRoutes from './idp-initiated-auth-config.js';
|
||||||
import {
|
import {
|
||||||
fetchConnectorProviderDetails,
|
fetchConnectorProviderDetails,
|
||||||
parseConnectorConfig,
|
parseConnectorConfig,
|
||||||
|
@ -300,4 +302,8 @@ export default function singleSignOnConnectorsRoutes<T extends ManagementApiRout
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (EnvSet.values.isDevFeaturesEnabled) {
|
||||||
|
ssoConnectorIdpInitiatedAuthConfigRoutes(...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import {
|
import {
|
||||||
|
type CreateSsoConnectorIdpInitiatedAuthConfig,
|
||||||
SsoProviderName,
|
SsoProviderName,
|
||||||
type CreateSsoConnector,
|
type CreateSsoConnector,
|
||||||
type SsoConnector,
|
type SsoConnector,
|
||||||
type SsoConnectorProvidersResponse,
|
type SsoConnectorProvidersResponse,
|
||||||
|
type SsoConnectorIdpInitiatedAuthConfig,
|
||||||
} from '@logto/schemas';
|
} from '@logto/schemas';
|
||||||
|
|
||||||
|
import { metadataXml } from '#src/__mocks__/sso-connectors-mock.js';
|
||||||
import { authedAdminApi } from '#src/api/api.js';
|
import { authedAdminApi } from '#src/api/api.js';
|
||||||
import { logtoUrl } from '#src/constants.js';
|
import { logtoUrl } from '#src/constants.js';
|
||||||
import { randomString } from '#src/utils.js';
|
import { randomString } from '#src/utils.js';
|
||||||
|
@ -65,6 +68,20 @@ export class SsoConnectorApi {
|
||||||
return connector;
|
return connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createMockSamlConnector(domains: string[], connectorName?: string) {
|
||||||
|
const connector = await this.create({
|
||||||
|
providerName: SsoProviderName.SAML,
|
||||||
|
connectorName: connectorName ?? `test-saml-${randomString()}`,
|
||||||
|
domains,
|
||||||
|
config: {
|
||||||
|
metadata: metadataXml,
|
||||||
|
},
|
||||||
|
syncProfile: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return connector;
|
||||||
|
}
|
||||||
|
|
||||||
async create(data: Partial<CreateSsoConnector>): Promise<SsoConnector> {
|
async create(data: Partial<CreateSsoConnector>): Promise<SsoConnector> {
|
||||||
const connector = await createSsoConnector(data);
|
const connector = await createSsoConnector(data);
|
||||||
|
|
||||||
|
@ -83,6 +100,21 @@ export class SsoConnectorApi {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setSsoConnectorIdpInitiatedAuthConfig(data: CreateSsoConnectorIdpInitiatedAuthConfig) {
|
||||||
|
const { connectorId, ...rest } = data;
|
||||||
|
return authedAdminApi
|
||||||
|
.put(`sso-connectors/${connectorId}/idp-initiated-auth-config`, {
|
||||||
|
json: rest,
|
||||||
|
})
|
||||||
|
.json<SsoConnectorIdpInitiatedAuthConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSsoConnectorIdpInitiatedAuthConfig(connectorId: string) {
|
||||||
|
return authedAdminApi
|
||||||
|
.get(`sso-connectors/${connectorId}/idp-initiated-auth-config`)
|
||||||
|
.json<SsoConnectorIdpInitiatedAuthConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
get firstConnectorId() {
|
get firstConnectorId() {
|
||||||
return Array.from(this.connectorInstances.keys())[0];
|
return Array.from(this.connectorInstances.keys())[0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { ApplicationType, type SsoConnector, type Application } from '@logto/schemas';
|
||||||
|
|
||||||
|
import { createApplication, deleteApplication } from '#src/api/application.js';
|
||||||
|
import { SsoConnectorApi } from '#src/api/sso-connector.js';
|
||||||
|
import { expectRejects } from '#src/helpers/index.js';
|
||||||
|
import { randomString, devFeatureTest } from '#src/utils.js';
|
||||||
|
|
||||||
|
devFeatureTest.describe('SAML IdP initiated authentication config', () => {
|
||||||
|
const ssoConnectorsApi = new SsoConnectorApi();
|
||||||
|
const applications = new Map<string, Application>();
|
||||||
|
const ssoConnectors = new Map<string, SsoConnector>();
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const [samlConnector, oidcConnector] = await Promise.all([
|
||||||
|
ssoConnectorsApi.createMockSamlConnector(['example.com']),
|
||||||
|
ssoConnectorsApi.createMockOidcConnector(['example.com']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ssoConnectors.set('saml', samlConnector);
|
||||||
|
ssoConnectors.set('oidc', oidcConnector);
|
||||||
|
|
||||||
|
const [spaApplication, webApplication, thirdPartyApplication] = await Promise.all([
|
||||||
|
createApplication(`spa-app-${randomString()}`, ApplicationType.SPA),
|
||||||
|
createApplication(`web-app-${randomString()}`, ApplicationType.Traditional),
|
||||||
|
createApplication(`third-party-app-${randomString()}`, ApplicationType.Traditional, {
|
||||||
|
isThirdParty: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
applications.set('spa', spaApplication);
|
||||||
|
applications.set('traditional', webApplication);
|
||||||
|
applications.set('thirdParty', thirdPartyApplication);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(applications.values()).map(async (app) => deleteApplication(app.id))
|
||||||
|
);
|
||||||
|
await ssoConnectorsApi.cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw 404 if the connector is not found', async () => {
|
||||||
|
const defaultApplicationId = applications.get('traditional')!.id;
|
||||||
|
|
||||||
|
await expectRejects(
|
||||||
|
ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId: 'not-found',
|
||||||
|
defaultApplicationId,
|
||||||
|
redirectUri: 'https://example.com',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'entity.not_exists_with_id',
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw 400 if the connector is not SAML', async () => {
|
||||||
|
const defaultApplicationId = applications.get('traditional')!.id;
|
||||||
|
|
||||||
|
await expectRejects(
|
||||||
|
ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId: ssoConnectors.get('oidc')!.id,
|
||||||
|
defaultApplicationId,
|
||||||
|
redirectUri: 'https://example.com',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'connector.saml_only_idp_initiated_auth',
|
||||||
|
status: 400,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw 404 if the application is not found', async () => {
|
||||||
|
await expectRejects(
|
||||||
|
ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId: ssoConnectors.get('saml')!.id,
|
||||||
|
defaultApplicationId: 'not-found',
|
||||||
|
redirectUri: 'https://example.com',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'entity.not_exists_with_id',
|
||||||
|
status: 404,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['spa', 'thirdParty'])(
|
||||||
|
'should throw 400 if the application is not a first-party traditional web application',
|
||||||
|
async (applicationKey) => {
|
||||||
|
const defaultApplicationId = applications.get(applicationKey)!.id;
|
||||||
|
await expectRejects(
|
||||||
|
ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId: ssoConnectors.get('saml')!.id,
|
||||||
|
defaultApplicationId,
|
||||||
|
redirectUri: 'https://example.com',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'connector.saml_idp_initiated_auth_invalid_application_type',
|
||||||
|
status: 400,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should create a new IdP-initiated authentication configuration for a SAML SSO connector', async () => {
|
||||||
|
const defaultApplicationId = applications.get('traditional')!.id;
|
||||||
|
const redirectUri = 'https://example.com';
|
||||||
|
const authParameters = {
|
||||||
|
resources: ['resource1', 'resource2'],
|
||||||
|
scopes: ['profile', 'email'],
|
||||||
|
};
|
||||||
|
const connectorId = ssoConnectors.get('saml')!.id;
|
||||||
|
|
||||||
|
const config = await ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId,
|
||||||
|
defaultApplicationId,
|
||||||
|
redirectUri,
|
||||||
|
authParameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).toMatchObject({
|
||||||
|
defaultApplicationId,
|
||||||
|
redirectUri,
|
||||||
|
authParameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedConfig = await ssoConnectorsApi.getSsoConnectorIdpInitiatedAuthConfig(connectorId);
|
||||||
|
|
||||||
|
expect(fetchedConfig).toMatchObject(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cascade delete the IdP-initiated authentication configuration when the application is deleted', async () => {
|
||||||
|
const application = await createApplication(
|
||||||
|
`web-app-${randomString()}`,
|
||||||
|
ApplicationType.Traditional
|
||||||
|
);
|
||||||
|
const connectorId = ssoConnectors.get('saml')!.id;
|
||||||
|
|
||||||
|
const config = await ssoConnectorsApi.setSsoConnectorIdpInitiatedAuthConfig({
|
||||||
|
connectorId,
|
||||||
|
defaultApplicationId: application.id,
|
||||||
|
redirectUri: 'https://example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config).not.toBeNull();
|
||||||
|
|
||||||
|
await deleteApplication(application.id);
|
||||||
|
|
||||||
|
await expectRejects(ssoConnectorsApi.getSsoConnectorIdpInitiatedAuthConfig(connectorId), {
|
||||||
|
code: 'entity.not_found',
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -35,6 +35,9 @@ const connector = {
|
||||||
'You can not have multiple social connectors that have same target and platform.',
|
'You can not have multiple social connectors that have same target and platform.',
|
||||||
cannot_overwrite_metadata_for_non_standard_connector:
|
cannot_overwrite_metadata_for_non_standard_connector:
|
||||||
"This connector's 'metadata' cannot be overwritten.",
|
"This connector's 'metadata' cannot be overwritten.",
|
||||||
|
saml_only_idp_initiated_auth: 'Only the SAML connector can set up IdP initiated authentication.',
|
||||||
|
saml_idp_initiated_auth_invalid_application_type:
|
||||||
|
'SAML IdP initiated authentication can only be used with a first-party traditional web application.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Object.freeze(connector);
|
export default Object.freeze(connector);
|
||||||
|
|
|
@ -15,7 +15,7 @@ export const idpInitiatedAuthParamsGuard = z
|
||||||
resources: z.array(z.string()).optional(),
|
resources: z.array(z.string()).optional(),
|
||||||
scopes: z.array(z.string()).optional(),
|
scopes: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.catchall(z.string());
|
.catchall(z.unknown());
|
||||||
|
|
||||||
export type IdpInitiatedAuthParams = z.infer<typeof idpInitiatedAuthParamsGuard>;
|
export type IdpInitiatedAuthParams = z.infer<typeof idpInitiatedAuthParamsGuard>;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue