0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

refactor(core): remove SSO connector config partial update logic (#4958)

* refactor(core): remove SSO connector config partial update logic

remove SSO connector config partial update logic

* fix(test): fix integration tests

fix integration tests

* refactor(core): refactor SAML SSO idpConfig type (#4960)

refactor SAML SSO idepConfig type
This commit is contained in:
simeng-li 2023-11-24 14:21:59 +08:00 committed by GitHub
parent 199b7997e3
commit 3f84e724b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 153 additions and 289 deletions

View file

@ -1,11 +1,11 @@
import { SsoConnectors, jsonObjectGuard } from '@logto/schemas';
import { SsoConnectors } from '@logto/schemas';
import {
ssoConnectorFactoriesResponseGuard,
type SsoConnectorFactoryDetail,
ssoConnectorWithProviderConfigGuard,
} from '@logto/schemas';
import { generateStandardShortId } from '@logto/shared';
import { conditional, assert, yes } from '@silverhand/essentials';
import { conditional, assert } from '@silverhand/essentials';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
@ -97,13 +97,10 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
// Validate the connector domains if it's provided
validateConnectorDomains(rest.domains);
/*
Validate the connector config if it's provided.
Allow partial config settings on create.
*/
const parsedConfig = config && parseConnectorConfig(providerName, config, true);
const connectorId = generateStandardShortId();
// Validate the connector config if it's provided
const parsedConfig = config && parseConnectorConfig(providerName, config);
const connectorId = generateStandardShortId();
const connector = await ssoConnectors.insert({
id: connectorId,
providerName,
@ -229,56 +226,4 @@ export default function singleSignOnRoutes<T extends AuthedRouter>(...args: Rout
return next();
}
);
/* Patch update a single sign on connector's config by id */
router.patch(
`${pathname}/:id/config`,
koaGuard({
params: z.object({ id: z.string().min(1) }),
/**
* Allow partially validate the connector config, on the guide page after
* the SSO connector is created, we allow users to save incomplete config without validating it.
*/
query: z.object({ partialValidateConfig: z.string().optional() }),
body: jsonObjectGuard,
response: ssoConnectorWithProviderConfigGuard,
status: [200, 404, 422],
}),
async (ctx, next) => {
const {
params: { id },
body,
query: { partialValidateConfig },
} = ctx.guard;
const { providerName, config } = await getSsoConnectorById(id);
// Merge with existing config and revalidate
const parsedConfig = parseConnectorConfig(
providerName,
{
...config,
...body,
},
yes(partialValidateConfig)
);
const connector = await ssoConnectors.updateById(id, {
config: parsedConfig,
});
// Make the typescript happy
assert(
isSupportedSsoConnector(connector),
new RequestError({ code: 'connector.not_found', status: 404 })
);
// Fetch provider details for the connector
const connectorWithProviderDetails = await fetchConnectorProviderDetails(connector, tenantId);
ctx.body = connectorWithProviderDetails;
return next();
}
);
}

View file

@ -33,16 +33,10 @@ export const parseFactoryDetail = (
Throw error if the config is invalid.
Partially validate the config if allowPartial is true.
*/
export const parseConnectorConfig = (
providerName: SsoProviderName,
config: JsonObject,
allowPartial?: boolean
) => {
export const parseConnectorConfig = (providerName: SsoProviderName, config: JsonObject) => {
const factory = ssoConnectorFactories[providerName];
const result = allowPartial
? factory.configGuard.partial().safeParse(config)
: factory.configGuard.safeParse(config);
const result = factory.configGuard.safeParse(config);
if (!result.success) {
throw new RequestError({

View file

@ -1,5 +1,5 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { trySafe, type Optional } from '@silverhand/essentials';
import { type Optional } from '@silverhand/essentials';
import * as saml from 'samlify';
import { z } from 'zod';
@ -9,14 +9,13 @@ import {
type SamlConnectorConfig,
type ExtendedSocialUserInfo,
type SamlServiceProviderMetadata,
type SamlMetadata,
type SamlIdentityProviderMetadata,
samlIdentityProviderMetadataGuard,
} from '../types/saml.js';
import {
parseXmlMetadata,
getSamlMetadataXml,
fetchSamlMetadataXml,
handleSamlAssertion,
attributeMappingPostProcessor,
getExtendedUserInfoFromRawUserProfile,
@ -31,11 +30,15 @@ import {
* This class provides the basic functionality to connect with a SAML IdP.
* All the SAML single sign-on connector should extend this class.
*
* @property config The SAML connector config
* @property assertionConsumerServiceUrl The SAML connector's assertion consumer service URL {@link file://src/routes/authn.ts}
* @property _samlIdpMetadataXml The cached raw SAML metadata (in XML-format) from the raw SAML SSO connector config
* @property _idpConfig The input SAML connector config
* @property idpConfig The parsed SAML connector config, throws error if _idpConfig input is invalid
* @property serviceProviderMetadata The SAML service provider metadata
* @property serviceProviderMetadata.assertionConsumerServiceUrl The SAML connector's SP assertion consumer service URL {@link file://src/routes/authn.ts}
* @property serviceProviderMetadata.entityId spEntityId
*
* @property _samlIdpMetadata The parsed SAML IdP metadata cache from the SAML IdP instance
* @property _identityProvider The SAML identity provider instance cache
*
* @method getSamlSpProperties Get the SAML service provider properties.
* @method getSamlIdpMetadata Parse and return SAML config from the SAML connector config. Throws error if config is invalid.
* @method parseSamlAssertion Parse and store the SAML assertion from IdP.
* @method getSingleSignOnUrl Get the SAML SSO URL.
@ -43,51 +46,47 @@ import {
* @method getIdpMetadataJson Get manually configured IdP SAML metadata from the raw SAML SSO connector config.
*/
class SamlConnector {
private readonly assertionConsumerServiceUrl: string;
private readonly spEntityId: string;
private readonly serviceProviderMetadata: SamlServiceProviderMetadata;
private _samlIdpMetadataXml: Optional<string>;
readonly serviceProviderMetadata: SamlServiceProviderMetadata;
private _samlIdpMetadata: Optional<SamlIdentityProviderMetadata>;
private _identityProvider: Optional<saml.IdentityProviderInstance>;
// Allow _idpConfig input to be undefined when constructing the connector.
constructor(
private readonly config: SamlConnectorConfig,
tenantId: string,
ssoConnectorId: string
ssoConnectorId: string,
private readonly _idpConfig: SamlConnectorConfig | undefined
) {
const tenantEndpoint = getTenantEndpoint(tenantId, EnvSet.values);
this.assertionConsumerServiceUrl = buildAssertionConsumerServiceUrl(
const assertionConsumerServiceUrl = buildAssertionConsumerServiceUrl(
tenantEndpoint,
ssoConnectorId
);
this.spEntityId = buildSpEntityId(tenantEndpoint, ssoConnectorId);
const spEntityId = buildSpEntityId(tenantEndpoint, ssoConnectorId);
this.serviceProviderMetadata = {
entityId: this.spEntityId,
assertionConsumerServiceUrl: this.assertionConsumerServiceUrl,
entityId: spEntityId,
assertionConsumerServiceUrl,
};
}
/**
* @returns Properties of the SAML service provider.
*/
getSamlSpProperties() {
return this.serviceProviderMetadata;
}
/**
* Return parsed SAML metadata.
*
* @returns Parsed SAML metadata (contains both SP and IdP metadata).
* @remarks
* Since the SP config does not depend on the connector config,
* we allow the idpConfig to be undefined when constructing the connector.
*
* However, the connector config is required when getting the SAML IdP metadata.
* Therefore, we provide a getter to get the valid SAML connector config.
*/
async getSamlConfig(): Promise<SamlMetadata> {
const serviceProvider = this.serviceProviderMetadata;
const identityProvider = await trySafe(async () => this.getSamlIdpMetadata());
return { serviceProvider, identityProvider };
get idpConfig() {
if (!this._idpConfig) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, 'config not found');
}
return this._idpConfig;
}
/**
@ -98,15 +97,13 @@ class SamlConnector {
* @returns The parsed SAML assertion from IdP (with attribute mapping applied).
*/
async parseSamlAssertion(body: Record<string, unknown>): Promise<ExtendedSocialUserInfo> {
const { x509Certificate } = await this.getSamlIdpMetadata();
const profileMap = attributeMappingPostProcessor(this.config.attributeMapping);
const identityProvider = await this.getIdentityProvider();
const { x509Certificate } = await this.getSamlIdpMetadata();
// HandleSamlAssertion takes a HTTPResponse-like object, need to wrap body in a object.
const samlAssertionContent = await handleSamlAssertion({ body }, identityProvider, {
x509Certificate,
entityId: this.spEntityId,
entityId: this.serviceProviderMetadata.entityId,
});
const userProfileGuard = z.record(z.string().or(z.array(z.string())));
@ -118,6 +115,7 @@ class SamlConnector {
const rawUserProfile = rawProfileParseResult.data;
const profileMap = attributeMappingPostProcessor(this.idpConfig.attributeMapping);
return getExtendedUserInfoFromRawUserProfile(rawUserProfile, profileMap);
}
@ -128,19 +126,20 @@ class SamlConnector {
* @returns The SSO URL.
*/
async getSingleSignOnUrl(relayState: string) {
const { x509Certificate } = await this.getSamlIdpMetadata();
const identityProvider = await this.getIdentityProvider();
const { x509Certificate } = await this.getSamlIdpMetadata();
const { entityId, assertionConsumerServiceUrl } = this.serviceProviderMetadata;
try {
// eslint-disable-next-line new-cap
const serviceProvider = saml.ServiceProvider({
entityID: this.spEntityId,
entityID: entityId,
relayState,
signingCert: x509Certificate,
authnRequestsSigned: identityProvider.entityMeta.isWantAuthnRequestsSigned(), // Should align with IdP setting.
assertionConsumerService: [
{
Location: this.assertionConsumerServiceUrl,
Location: assertionConsumerServiceUrl,
Binding: saml.Constants.BindingNamespace.Post,
},
],
@ -161,13 +160,15 @@ class SamlConnector {
*
* @returns Parsed SAML config along with it's parsed metadata.
*/
protected async getSamlIdpMetadata(): Promise<SamlIdentityProviderMetadata> {
async getSamlIdpMetadata(): Promise<SamlIdentityProviderMetadata> {
if (this._samlIdpMetadata) {
return this._samlIdpMetadata;
}
const identityProvider = await this.getIdentityProvider();
this._samlIdpMetadata = parseXmlMetadata(identityProvider);
return this._samlIdpMetadata;
}
@ -177,12 +178,13 @@ class SamlConnector {
* @returns The raw SAML metadata in XML-format.
*/
private async getIdpMetadataXml() {
if (this._samlIdpMetadataXml) {
return this._samlIdpMetadataXml;
if ('metadataUrl' in this.idpConfig) {
return fetchSamlMetadataXml(this.idpConfig.metadataUrl);
}
this._samlIdpMetadataXml = await getSamlMetadataXml(this.config);
return this._samlIdpMetadataXml;
if ('metadata' in this.idpConfig) {
return this.idpConfig.metadata;
}
}
/**
@ -192,7 +194,7 @@ class SamlConnector {
*/
private getIdpMetadataJson() {
// Required fields of metadata should not be undefined.
const result = samlIdentityProviderMetadataGuard.safeParse(this.config);
const result = samlIdentityProviderMetadataGuard.safeParse(this.idpConfig);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
@ -211,7 +213,9 @@ class SamlConnector {
return this._identityProvider;
}
// If `metadataUrl` or `metadata` is provided, we use it to construct the identity provider.
const idpMetadataXml = await this.getIdpMetadataXml();
if (idpMetadataXml) {
// eslint-disable-next-line new-cap
this._identityProvider = saml.IdentityProvider({
@ -220,8 +224,9 @@ class SamlConnector {
return this._identityProvider;
}
const idpMetadataJson = this.getIdpMetadataJson();
const { entityId: entityID, signInEndpoint, x509Certificate } = idpMetadataJson;
// If `metadataUrl` and `metadata` are not provided, we use get metadata from the idpConfig directly
const { entityId: entityID, signInEndpoint, x509Certificate } = this.getIdpMetadataJson();
// eslint-disable-next-line new-cap
this._identityProvider = saml.IdentityProvider({
entityID,

View file

@ -8,7 +8,6 @@ import { z } from 'zod';
import { ssoPath } from '#src/routes/interaction/const.js';
import {
type SamlConnectorConfig,
defaultAttributeMapping,
type CustomizableAttributeMap,
type AttributeMap,
@ -57,7 +56,7 @@ export const parseXmlMetadata = (
const result = samlIdentityProviderMetadataGuard.safeParse(rawSamlMetadata);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
throw new ConnectorError(ConnectorErrorCodes.InvalidMetadata, result.error);
}
return result.data;
@ -69,28 +68,21 @@ export const parseXmlMetadata = (
* @param config The raw SAML SSO connector config.
* @returns The corresponding IdP's raw SAML metadata (in XML format).
*/
export const getSamlMetadataXml = async (
config: SamlConnectorConfig
): Promise<Optional<string>> => {
const { metadata, metadataUrl } = config;
if (metadataUrl) {
try {
const { body } = await got.get(metadataUrl);
export const fetchSamlMetadataXml = async (metadataUrl: string): Promise<Optional<string>> => {
try {
const { body } = await got.get(metadataUrl);
const result = z.string().safeParse(body);
const result = z.string().safeParse(body);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
return result.data;
} catch (error: unknown) {
// HTTP request error
throw new ConnectorError(ConnectorErrorCodes.General, error);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
}
return metadata;
return result.data;
} catch (error: unknown) {
// HTTP request error
throw new ConnectorError(ConnectorErrorCodes.General, error);
}
};
/**

View file

@ -3,16 +3,13 @@ import { SsoProviderName } from '@logto/schemas';
import { mockSsoConnector as _mockSsoConnector } from '#src/__mocks__/sso.js';
import { type SamlConnectorConfig } from '../types/saml.js';
import { samlSsoConnectorFactory } from './index.js';
const mockSsoConnector = { ..._mockSsoConnector, providerName: SsoProviderName.SAML };
describe('SamlSsoConnector', () => {
it('SamlSsoConnector should contains static properties', () => {
expect(samlSsoConnectorFactory.providerName).toEqual(SsoProviderName.SAML);
expect(samlSsoConnectorFactory.configGuard).toBeDefined();
});
it('constructor should work properly', () => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const createSamlSsoConnector = () =>
@ -21,20 +18,43 @@ describe('SamlSsoConnector', () => {
expect(createSamlSsoConnector).not.toThrow();
});
it('constructor should throw error if config is invalid', () => {
it('should get SP config properly without proper IdP config', async () => {
const temporaryMockSsoConnector = { ...mockSsoConnector, config: { metadata: 123 } };
const result = samlSsoConnectorFactory.configGuard.safeParse(temporaryMockSsoConnector.config);
const connector = new samlSsoConnectorFactory.constructor(
temporaryMockSsoConnector,
'default_tenant'
);
if (result.success) {
throw new Error('Invalid config');
}
const { serviceProvider, identityProvider } = await connector.getConfig();
expect(serviceProvider.entityId).not.toBeUndefined();
expect(serviceProvider.assertionConsumerServiceUrl).not.toBeUndefined();
expect(identityProvider).toBeUndefined();
});
const createSamlSsoConnector = () => {
return new samlSsoConnectorFactory.constructor(temporaryMockSsoConnector, 'default_tenant');
};
it('should throw error on calling getIdpMetadata, if the config is invalid', async () => {
const connector = new samlSsoConnectorFactory.constructor(mockSsoConnector, 'default_tenant');
expect(createSamlSsoConnector).toThrow(
new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error)
await expect(async () => connector.getSamlIdpMetadata()).rejects.toThrow(
new ConnectorError(ConnectorErrorCodes.InvalidConfig, 'config not found')
);
});
it.each([
{ metadata: 'metadata' },
{ metadataUrl: 'https://example.com' },
{
entityId: '123',
signInEndpoint: 'https://example.com',
x509Certificate: 'mockCert',
},
])('should parse config with %p successfully', (config: SamlConnectorConfig) => {
const temporaryMockSsoConnector = { ...mockSsoConnector, config };
const connector = new samlSsoConnectorFactory.constructor(
temporaryMockSsoConnector,
'default_tenant'
);
expect(connector.idpConfig).toEqual(config);
});
});

View file

@ -1,12 +1,12 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { type SsoConnector, SsoProviderName } from '@logto/schemas';
import { conditional, trySafe } from '@silverhand/essentials';
import assertThat from '#src/utils/assert-that.js';
import SamlConnector from '../SamlConnector/index.js';
import { type SingleSignOnFactory } from '../index.js';
import { type SingleSignOn } from '../types/index.js';
import { samlConnectorConfigGuard } from '../types/saml.js';
import { samlConnectorConfigGuard, type SamlMetadata } from '../types/saml.js';
import {
type SingleSignOnConnectorSession,
type CreateSingleSignOnSession,
@ -18,10 +18,11 @@ import {
* This class extends the basic SAML connector class and add some business related utils methods.
*
* @property data The SAML connector data from the database
* @property inValidConnectorConfig Whether the connector config is invalid
*
* @method getProperties Get the SAML service provider properties.
* @method getConfig Get parsed SAML config along with it's metadata. Throws error if config is invalid.
* @method getUserInfo Get social user info.
* @method getConfig Get the SP and IdP metadata
* @method getUserInfo Get user info from the SAML assertion.
* @method getAuthorizationUrl Get the SAML SSO URL.
*/
export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
constructor(
@ -30,11 +31,8 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
) {
const parseConfigResult = samlConnectorConfigGuard.safeParse(data.config);
if (!parseConfigResult.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, parseConfigResult.error);
}
super(parseConfigResult.data, tenantId, data.id);
// Fallback to undefined if config is invalid
super(tenantId, data.id, conditional(parseConfigResult.success && parseConfigResult.data));
}
async getIssuer() {
@ -44,12 +42,17 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
}
/**
* Get parsed SAML connector's config along with it's metadata. Throws error if config is invalid.
*
* @returns Parsed SAML connector config and it's metadata.
* ServiceProvider: SP metadata
* identityProvider: IdP metadata. Returns undefined if the idp config is invalid.
*/
async getConfig() {
return this.getSamlConfig();
async getConfig(): Promise<SamlMetadata> {
const serviceProvider = this.serviceProviderMetadata;
const identityProvider = await trySafe(async () => this.getSamlIdpMetadata());
return {
serviceProvider,
identityProvider,
};
}
/**

View file

@ -32,17 +32,24 @@ export const samlIdentityProviderMetadataGuard = z.object({
signInEndpoint: z.string(),
x509Certificate: z.string(),
});
export type SamlIdentityProviderMetadata = z.infer<typeof samlIdentityProviderMetadataGuard>;
export const samlConnectorConfigGuard = samlIdentityProviderMetadataGuard
.extend({
attributeMapping: customizableAttributeMappingGuard,
metadata: z.string(),
export const samlConnectorConfigGuard = z.union([
// Config using Metadata URL
z.object({
metadataUrl: z.string(),
})
.partial();
attributeMapping: customizableAttributeMappingGuard.optional(),
}),
// Config using Metadata XML
z.object({
metadata: z.string(),
attributeMapping: customizableAttributeMappingGuard.optional(),
}),
// Config using Metadata detail
samlIdentityProviderMetadataGuard.extend({
attributeMapping: customizableAttributeMappingGuard.optional(),
}),
]);
export type SamlConnectorConfig = z.infer<typeof samlConnectorConfigGuard>;
const samlMetadataGuard = z.object({

View file

@ -1,5 +1,4 @@
import { type CreateSsoConnector, type SsoConnector } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { authedAdminApi } from '#src/api/api.js';
@ -44,15 +43,3 @@ export const patchSsoConnectorById = async (id: string, data: Partial<SsoConnect
json: data,
})
.json<SsoConnectorWithProviderConfig>();
export const patchSsoConnectorConfigById = async (
id: string,
data: Record<string, unknown>,
partialValidate = false
) =>
authedAdminApi
.patch(`sso-connectors/${id}/config`, {
json: data,
...conditional(partialValidate && { searchParams: { partialValidateConfig: 'true' } }),
})
.json<SsoConnectorWithProviderConfig>();

View file

@ -35,10 +35,6 @@ describe('Single Sign On Sad Path', () => {
const { id } = await createSsoConnector({
providerName: SsoProviderName.OIDC,
connectorName: 'test-oidc',
config: {
clientId: 'foo',
clientSecret: 'bar',
},
});
const client = await initClient();

View file

@ -1,5 +1,4 @@
import { SsoProviderName } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { HTTPError } from 'got';
import {
@ -13,7 +12,6 @@ import {
getSsoConnectorById,
deleteSsoConnectorById,
patchSsoConnectorById,
patchSsoConnectorConfigById,
} from '#src/api/sso-connector.js';
describe('sso-connector library', () => {
@ -75,41 +73,29 @@ describe('post sso-connectors', () => {
await deleteSsoConnectorById(response.id);
});
it.each(providerNames)('should throw if invalid config is provided', async (providerName) => {
it('OIDC connector should throw error if insufficient config is provided', async () => {
await expect(
createSsoConnector({
providerName,
providerName: SsoProviderName.OIDC,
connectorName: 'test',
config: {
issuer: 23,
signInEndpoint: 123,
clientId: 'logto.io',
},
})
).rejects.toThrow(HTTPError);
});
it.each(partialConfigAndProviderNames)(
'should create a new sso connector with partial configs',
async ({ providerName, config }) => {
const data = {
providerName,
it('SAML connector should throw error if invalid config is provided', async () => {
await expect(
createSsoConnector({
providerName: SsoProviderName.SAML,
connectorName: 'test',
config,
domains: ['test.com'],
};
const response = await createSsoConnector(data);
expect(response).toHaveProperty('id');
expect(response).toHaveProperty('providerName', providerName);
expect(response).toHaveProperty('connectorName', 'test');
expect(response).toHaveProperty('config', data.config);
expect(response).toHaveProperty('domains', data.domains);
expect(response).toHaveProperty('syncProfile', false);
await deleteSsoConnectorById(response.id);
}
);
config: {
entityId: 123,
},
})
).rejects.toThrow(HTTPError);
});
});
describe('get sso-connectors', () => {
@ -275,75 +261,3 @@ describe('patch sso-connector by id', () => {
}
);
});
describe('patch sso-connector config by id', () => {
it('should return 404 if connector is not found', async () => {
await expect(patchSsoConnectorConfigById('invalid-id', {})).rejects.toThrow(HTTPError);
});
it.each(providerNames)('should throw if invalid config is provided', async (providerName) => {
const { id } = await createSsoConnector({
providerName,
connectorName: 'integration_test connector',
config: {
clientSecret: 'bar',
metadataType: 'URL',
},
});
await expect(
patchSsoConnectorConfigById(id, {
issuer: 23,
signInEndpoint: 123,
})
).rejects.toThrow(HTTPError);
await deleteSsoConnectorById(id);
});
it.each(providerNames)(
'should successfully patch incomplete config if `partialValidateConfig` query parameter is specified',
async (providerName) => {
const { id } = await createSsoConnector({
providerName,
connectorName: 'integration_test connector',
config: {
clientSecret: 'bar',
metadataType: 'URL',
},
});
await expect(
patchSsoConnectorConfigById(
id,
{
...conditional(providerName === SsoProviderName.OIDC && { issuer: 'issuer' }),
...conditional(providerName === SsoProviderName.SAML && { entityId: 'entity_id' }),
},
true
)
).resolves.not.toThrow();
await deleteSsoConnectorById(id);
}
);
it.each(partialConfigAndProviderNames)(
'should patch sso connector config',
async ({ providerName, config }) => {
const { id } = await createSsoConnector({
providerName,
connectorName: 'integration_test connector',
});
const connector = await patchSsoConnectorConfigById(id, config);
expect(connector).toHaveProperty('id', id);
expect(connector).toHaveProperty('providerName', providerName);
expect(connector).toHaveProperty('connectorName', 'integration_test connector');
expect(connector).toHaveProperty('config', config);
await deleteSsoConnectorById(id);
}
);
});

View file

@ -82,7 +82,7 @@ describe('.well-known api', () => {
it('should filter out the sso connectors with invalid config', async () => {
const { id } = await createSsoConnector({
...newOidcSsoConnectorPayload,
config: {},
config: undefined,
});
const signInExperience = await api

View file

@ -63,6 +63,7 @@ export const ssoConnectorFactoriesResponseGuard = z.object({
export type SsoConnectorFactoriesResponse = z.infer<typeof ssoConnectorFactoriesResponseGuard>;
// API response guard for all the SSO connectors CRUD APIs
export const ssoConnectorWithProviderConfigGuard = SsoConnectors.guard
.omit({ providerName: true })
.merge(