mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(core,test): fix saml sp config and refactor saml class internal util methods (#4878)
This commit is contained in:
parent
5b0ea2192b
commit
5832b30276
5 changed files with 219 additions and 141 deletions
|
@ -1,24 +1,27 @@
|
|||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import { assert, appendPath, conditional, type Optional } from '@silverhand/essentials';
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
import * as saml from 'samlify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||
import { ssoPath } from '#src/routes/interaction/const.js';
|
||||
|
||||
import {
|
||||
type SamlConfig,
|
||||
type SamlConnectorConfig,
|
||||
samlMetadataGuard,
|
||||
type ExtendedSocialUserInfo,
|
||||
type SamlServiceProviderMetadata,
|
||||
type SamlMetadata,
|
||||
type SamlIdentityProviderMetadata,
|
||||
samlIdentityProviderMetadataGuard,
|
||||
} from '../types/saml.js';
|
||||
|
||||
import {
|
||||
parseXmlMetadata,
|
||||
getRawSamlMetadata,
|
||||
getSamlMetadataXml,
|
||||
handleSamlAssertion,
|
||||
attributeMappingPostProcessor,
|
||||
getExtendedUserInfoFromRawUserProfile,
|
||||
buildSpEntityId,
|
||||
buildAssertionConsumerServiceUrl,
|
||||
} from './utils.js';
|
||||
|
||||
/**
|
||||
|
@ -29,66 +32,52 @@ import {
|
|||
* All the SAML single sign-on connector should extend this class.
|
||||
*
|
||||
* @property config The SAML connector config
|
||||
* @property acsUrl The SAML connector's assertion consumer service URL {@link file://src/routes/authn.ts}
|
||||
* @property _rawSamlMetadata The cached raw SAML metadata (in XML-format) from the raw SAML SSO connector config
|
||||
* @property _parsedSamlMetadata The cached parsed SAML metadata from the raw SAML SSO connector config
|
||||
* @property _samlAssertionContent The cached parsed SAML assertion from IdP (with attribute mapping applied)
|
||||
* @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
|
||||
*
|
||||
* @method getSamlConfig Parse and return SAML config from the XML-format metadata. Throws error if config is invalid.
|
||||
* @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.
|
||||
* @method getIdpXmlMetadata Get the raw SAML metadata (in XML-format) from the raw SAML SSO connector config.
|
||||
* @method getIdpMetadataXml Get the raw SAML metadata (in XML-format) from the raw SAML SSO connector config.
|
||||
* @method getIdpMetadataJson Get manually configured IdP SAML metadata from the raw SAML SSO connector config.
|
||||
*/
|
||||
class SamlConnector {
|
||||
readonly acsUrl: string;
|
||||
private _rawSamlMetadata: Optional<string>;
|
||||
private _parsedSamlMetadata: Optional<SamlConfig>;
|
||||
private readonly assertionConsumerServiceUrl: string;
|
||||
private readonly spEntityId: string;
|
||||
private readonly serviceProviderMetadata: SamlServiceProviderMetadata;
|
||||
|
||||
private _samlIdpMetadataXml: Optional<string>;
|
||||
|
||||
private _samlIdpMetadata: Optional<SamlIdentityProviderMetadata>;
|
||||
private _identityProvider: Optional<saml.IdentityProviderInstance>;
|
||||
|
||||
constructor(
|
||||
private readonly config: SamlConnectorConfig,
|
||||
tenantId: string,
|
||||
ssoConnectorId: string
|
||||
) {
|
||||
this.acsUrl = appendPath(
|
||||
this.assertionConsumerServiceUrl = buildAssertionConsumerServiceUrl(
|
||||
getTenantEndpoint(tenantId, EnvSet.values),
|
||||
`api/authn/${ssoPath}/saml/${ssoConnectorId}`
|
||||
).toString();
|
||||
ssoConnectorId
|
||||
);
|
||||
|
||||
this.spEntityId = buildSpEntityId(EnvSet.values, tenantId, ssoConnectorId);
|
||||
|
||||
this.serviceProviderMetadata = {
|
||||
entityId: this.spEntityId,
|
||||
assertionConsumerServiceUrl: this.assertionConsumerServiceUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SAML config along with parsed metadata from raw SAML SSO connector config.
|
||||
* Return parsed SAML metadata.
|
||||
*
|
||||
* @returns Parsed SAML config along with it's parsed metadata.
|
||||
* @returns Parsed SAML metadata (contains both SP and IdP metadata).
|
||||
*/
|
||||
async getSamlConfig(): Promise<SamlConfig> {
|
||||
if (this._parsedSamlMetadata) {
|
||||
return this._parsedSamlMetadata;
|
||||
}
|
||||
|
||||
// Get raw SAML metadata ready.
|
||||
await this.getIdpXmlMetadata();
|
||||
|
||||
const samlConfig = conditional(
|
||||
this._rawSamlMetadata && parseXmlMetadata(this._rawSamlMetadata)
|
||||
);
|
||||
|
||||
if (samlConfig) {
|
||||
this._parsedSamlMetadata = { ...samlConfig, ...this.config };
|
||||
return this._parsedSamlMetadata;
|
||||
}
|
||||
|
||||
// Required fields of metadata should not be undefined.
|
||||
const result = samlMetadataGuard
|
||||
.pick({ signInEndpoint: true, x509Certificate: true, entityId: true })
|
||||
.safeParse(this.config);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
||||
}
|
||||
|
||||
// Simply return `this.config` should be of SamlConfig type, but seems the type inference is not that smart.
|
||||
this._parsedSamlMetadata = { ...this.config, ...result.data };
|
||||
return this._parsedSamlMetadata;
|
||||
async getSamlConfig(): Promise<SamlMetadata> {
|
||||
const serviceProvider = this.serviceProviderMetadata;
|
||||
const identityProvider = await this.getSamlIdpMetadata();
|
||||
return { serviceProvider, identityProvider };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,21 +88,13 @@ class SamlConnector {
|
|||
* @returns The parsed SAML assertion from IdP (with attribute mapping applied).
|
||||
*/
|
||||
async parseSamlAssertion(assertion: Record<string, unknown>): Promise<ExtendedSocialUserInfo> {
|
||||
const parsedConfig = await this.getSamlConfig();
|
||||
const profileMap = attributeMappingPostProcessor(parsedConfig.attributeMapping);
|
||||
const idpMetadataXml = await this.getIdpXmlMetadata();
|
||||
const { x509Certificate } = await this.getSamlIdpMetadata();
|
||||
const profileMap = attributeMappingPostProcessor(this.config.attributeMapping);
|
||||
|
||||
// Add SSO connector errors and replace connector errors.
|
||||
assert(
|
||||
idpMetadataXml,
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
|
||||
message: "Can not get identity provider's metadata, please check configuration.",
|
||||
})
|
||||
);
|
||||
|
||||
const samlAssertionContent = await handleSamlAssertion(assertion, {
|
||||
...parsedConfig,
|
||||
idpMetadataXml,
|
||||
const identityProvider = await this.getIdentityProvider();
|
||||
const samlAssertionContent = await handleSamlAssertion(assertion, identityProvider, {
|
||||
x509Certificate,
|
||||
entityId: this.spEntityId,
|
||||
});
|
||||
|
||||
const userProfileGuard = z.record(z.string().or(z.array(z.string())));
|
||||
|
@ -135,38 +116,19 @@ class SamlConnector {
|
|||
* @returns The SSO URL.
|
||||
*/
|
||||
async getSingleSignOnUrl(relayState: string) {
|
||||
const {
|
||||
entityId: entityID,
|
||||
x509Certificate,
|
||||
nameIdFormat,
|
||||
signingAlgorithm,
|
||||
} = await this.getSamlConfig();
|
||||
const { x509Certificate } = await this.getSamlIdpMetadata();
|
||||
const identityProvider = await this.getIdentityProvider();
|
||||
|
||||
try {
|
||||
const idpMetadataXml = await this.getIdpXmlMetadata();
|
||||
// Add SSO connector errors and replace connector errors.
|
||||
assert(
|
||||
idpMetadataXml,
|
||||
new ConnectorError(ConnectorErrorCodes.InvalidConfig, {
|
||||
message: "Can not get identity provider's metadata, please check configuration.",
|
||||
})
|
||||
);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const identityProvider = saml.IdentityProvider({
|
||||
metadata: idpMetadataXml,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const serviceProvider = saml.ServiceProvider({
|
||||
entityID, // FIXME: @darcyYe
|
||||
entityID: this.spEntityId,
|
||||
relayState,
|
||||
nameIDFormat: nameIdFormat,
|
||||
signingCert: x509Certificate,
|
||||
requestSignatureAlgorithm: signingAlgorithm,
|
||||
authnRequestsSigned: identityProvider.entityMeta.isWantAuthnRequestsSigned(), // Should align with IdP setting.
|
||||
assertionConsumerService: [
|
||||
{
|
||||
Location: this.acsUrl,
|
||||
Location: this.assertionConsumerServiceUrl,
|
||||
Binding: saml.Constants.BindingNamespace.Post,
|
||||
},
|
||||
],
|
||||
|
@ -185,16 +147,85 @@ class SamlConnector {
|
|||
*
|
||||
* @returns The raw SAML metadata in XML-format.
|
||||
*/
|
||||
private async getIdpXmlMetadata() {
|
||||
if (this._rawSamlMetadata) {
|
||||
return this._rawSamlMetadata;
|
||||
private async getIdpMetadataXml() {
|
||||
if (this._samlIdpMetadataXml) {
|
||||
return this._samlIdpMetadataXml;
|
||||
}
|
||||
|
||||
const rawSamlMetadata = await getRawSamlMetadata(this.config);
|
||||
if (rawSamlMetadata) {
|
||||
this._rawSamlMetadata = rawSamlMetadata;
|
||||
this._samlIdpMetadataXml = await getSamlMetadataXml(this.config);
|
||||
return this._samlIdpMetadataXml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manually filled SAML IdP metadata from the raw SAML SSO connector config (including `signInEndpoint` and `x509Certificate`).
|
||||
*
|
||||
* @returns Manually filled SAML IdP metadata.
|
||||
*/
|
||||
private getIdpMetadataJson() {
|
||||
// Required fields of metadata should not be undefined.
|
||||
const result = samlIdentityProviderMetadataGuard.safeParse(this.config);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
||||
}
|
||||
return this._rawSamlMetadata;
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identity provider constructed using `metadata` got from `config`.
|
||||
*
|
||||
* @returns Identity provider instance.
|
||||
*/
|
||||
private async getIdentityProvider() {
|
||||
if (this._identityProvider) {
|
||||
return this._identityProvider;
|
||||
}
|
||||
|
||||
const idpMetadataXml = await this.getIdpMetadataXml();
|
||||
if (idpMetadataXml) {
|
||||
// eslint-disable-next-line new-cap
|
||||
this._identityProvider = saml.IdentityProvider({
|
||||
metadata: idpMetadataXml,
|
||||
});
|
||||
return this._identityProvider;
|
||||
}
|
||||
|
||||
const idpMetadataJson = this.getIdpMetadataJson();
|
||||
const { entityId: entityID, signInEndpoint, x509Certificate } = idpMetadataJson;
|
||||
// eslint-disable-next-line new-cap
|
||||
this._identityProvider = saml.IdentityProvider({
|
||||
entityID,
|
||||
signingCert: x509Certificate,
|
||||
/**
|
||||
* When `metadata` is not provided, `signInEndpoint` and `x509Certificate` are ensured by previous guard.
|
||||
* We only support redirect binding for now when sending SAML auth request.
|
||||
*/
|
||||
singleSignOnService: [
|
||||
{
|
||||
Location: signInEndpoint,
|
||||
Binding: saml.Constants.BindingNamespace.Redirect,
|
||||
},
|
||||
],
|
||||
});
|
||||
return this._identityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SAML IdP config along with parsed metadata from raw SAML SSO connector config.
|
||||
*
|
||||
* @remarks If this function can successfully get the SAML metadata, then it guarantees that the SAML identity provider instance is initiated.
|
||||
*
|
||||
* @returns Parsed SAML config along with it's parsed metadata.
|
||||
*/
|
||||
private async getSamlIdpMetadata(): Promise<SamlIdentityProviderMetadata> {
|
||||
if (this._samlIdpMetadata) {
|
||||
return this._samlIdpMetadata;
|
||||
}
|
||||
|
||||
const identityProvider = await this.getIdentityProvider();
|
||||
this._samlIdpMetadata = parseXmlMetadata(identityProvider);
|
||||
return this._samlIdpMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
import * as validator from '@authenio/samlify-node-xmllint';
|
||||
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
||||
import { type Optional, conditional } from '@silverhand/essentials';
|
||||
import { type GlobalValues } from '@logto/shared';
|
||||
import { type Optional, conditional, appendPath } from '@silverhand/essentials';
|
||||
import { got } from 'got';
|
||||
import * as saml from 'samlify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ssoPath } from '#src/routes/interaction/const.js';
|
||||
|
||||
import {
|
||||
samlMetadataGuard,
|
||||
type SamlMetadata,
|
||||
type SamlConnectorConfig,
|
||||
type SamlConfig,
|
||||
defaultAttributeMapping,
|
||||
type CustomizableAttributeMap,
|
||||
type AttributeMap,
|
||||
extendedSocialUserInfoGuard,
|
||||
type ExtendedSocialUserInfo,
|
||||
type SamlIdentityProviderMetadata,
|
||||
samlIdentityProviderMetadataGuard,
|
||||
} from '../types/saml.js';
|
||||
|
||||
type ESamlHttpRequest = Parameters<saml.ServiceProviderInstance['parseLoginResponse']>[2];
|
||||
|
||||
/**
|
||||
* Parse XML-format raw SAML metadata and return the parsed SAML metadata.
|
||||
* Return the parsed SAML metadata using SAML identity provider initiated with SAML metadata.
|
||||
*
|
||||
* @param xml Raw SAML metadata in XML format.
|
||||
* @returns The parsed SAML metadata.
|
||||
* @param idP SAML identity provider instance.
|
||||
*
|
||||
* @returns The parsed SAML IdP metadata.
|
||||
*/
|
||||
export const parseXmlMetadata = (xml: string): SamlMetadata => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const idP = saml.IdentityProvider({ metadata: xml });
|
||||
|
||||
export const parseXmlMetadata = (
|
||||
idP: saml.IdentityProviderInstance
|
||||
): SamlIdentityProviderMetadata => {
|
||||
// Used to check whether xml content is valid in format.
|
||||
saml.setSchemaValidator(validator);
|
||||
|
||||
|
@ -45,20 +47,15 @@ export const parseXmlMetadata = (xml: string): SamlMetadata => {
|
|||
|
||||
const rawSamlMetadata = {
|
||||
entityId: idP.entityMeta.getEntityID(),
|
||||
/**
|
||||
* See implementation in `samlify` {@link https://github.com/tngan/samlify/blob/55f845da60b18d40668885c7f7e71ed0967ef67f/src/entity.ts#L88}.
|
||||
*/
|
||||
nameIdFormat: idP.entitySetting.nameIDFormat,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
signInEndpoint: singleSignOnService,
|
||||
signingAlgorithm: idP.entitySetting.requestSignatureAlgorithm,
|
||||
// The type inference of the return type of `getX509Certificate` is any, will be guarded by later zod parser if it is not string-typed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
x509Certificate: idP.entityMeta.getX509Certificate(saml.Constants.wording.certUse.signing),
|
||||
};
|
||||
|
||||
// The return type of `samlify`
|
||||
const result = samlMetadataGuard.safeParse(rawSamlMetadata);
|
||||
const result = samlIdentityProviderMetadataGuard.safeParse(rawSamlMetadata);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
|
||||
|
@ -73,7 +70,7 @@ export const parseXmlMetadata = (xml: string): SamlMetadata => {
|
|||
* @param config The raw SAML SSO connector config.
|
||||
* @returns The corresponding IdP's raw SAML metadata (in XML format).
|
||||
*/
|
||||
export const getRawSamlMetadata = async (
|
||||
export const getSamlMetadataXml = async (
|
||||
config: SamlConnectorConfig
|
||||
): Promise<Optional<string>> => {
|
||||
const { metadata, metadataUrl } = config;
|
||||
|
@ -129,19 +126,16 @@ export const getExtendedUserInfoFromRawUserProfile = (
|
|||
* Handle the SAML assertion from the identity provider.
|
||||
*
|
||||
* @param request The SAML assertion sent by IdP (after getting the SAML auth request).
|
||||
* @param config The full config of the SAML SSO connector.
|
||||
* @param identityProvider The SAML identity provider instance (where we can get parsed IdP metadata).
|
||||
* @param metadata The selected part of metadata of the SAML SSO connector.
|
||||
* @returns The returned info contained in the SAML assertion.
|
||||
*/
|
||||
export const handleSamlAssertion = async (
|
||||
request: ESamlHttpRequest,
|
||||
config: SamlConfig & { idpMetadataXml: string }
|
||||
identityProvider: saml.IdentityProviderInstance,
|
||||
metadata: { entityId: string; x509Certificate: string }
|
||||
): Promise<Record<string, unknown>> => {
|
||||
const { entityId: entityID, x509Certificate, idpMetadataXml } = config;
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const identityProvider = saml.IdentityProvider({
|
||||
metadata: idpMetadataXml,
|
||||
});
|
||||
const { entityId: entityID, x509Certificate } = metadata;
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const serviceProvider = saml.ServiceProvider({
|
||||
|
@ -185,3 +179,37 @@ export const attributeMappingPostProcessor = (
|
|||
...conditional(attributeMapping && attributeMapping),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the entity id for the current SAML SSO connector using admin console path, current tenant id and connector id.
|
||||
* Used URL-like entity id here since some identity providers will check the format of the entity id.
|
||||
* See {@link https://spaces.at.internet2.edu/display/federation/saml-metadata-entityid} to know more details about how should `entityId` look like.
|
||||
*
|
||||
* @param globalValues Global setups
|
||||
* @param tenantId Current tenant id.
|
||||
* @param connectorId Current connector id.
|
||||
*
|
||||
* @returns Entity id for the current SAML SSO connector.
|
||||
*/
|
||||
export const buildSpEntityId = (
|
||||
globalValues: GlobalValues,
|
||||
tenantId: string,
|
||||
connectorId: string
|
||||
) => {
|
||||
const { isCloud, cloudUrlSet, adminUrlSet } = globalValues;
|
||||
return appendPath(
|
||||
isCloud ? cloudUrlSet.endpoint : adminUrlSet.endpoint,
|
||||
tenantId,
|
||||
`/enterprise-sso/${connectorId}`
|
||||
).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the assertion consumer service url for the current SAML SSO connector using current base url and connector id.
|
||||
*
|
||||
* @param baseUrl Base endpoint for the current service
|
||||
* @param ssoConnectorId Current enterprise SSO connector id
|
||||
* @returns
|
||||
*/
|
||||
export const buildAssertionConsumerServiceUrl = (baseUrl: URL, ssoConnectorId: string) =>
|
||||
appendPath(baseUrl, `api/authn/${ssoPath}/saml/${ssoConnectorId}`).toString();
|
||||
|
|
|
@ -38,7 +38,9 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
|
|||
}
|
||||
|
||||
async getIssuer() {
|
||||
const { entityId } = await this.getSamlConfig();
|
||||
const {
|
||||
serviceProvider: { entityId },
|
||||
} = await this.getSamlConfig();
|
||||
return entityId;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { socialUserInfoGuard } from '@logto/connector-kit';
|
||||
import { jsonGuard } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Since the SAML SSO user info will extend the basic social user info (will contain extra info like `organization`, `role` etc.), but for now we haven't decide what should be included in extended user info, so we just use the basic social user info guard here to keep SSOT.
|
||||
|
@ -14,33 +13,45 @@ const customizableAttributeMappingGuard = samlAttributeMappingGuard.partial();
|
|||
export type CustomizableAttributeMap = z.infer<typeof customizableAttributeMappingGuard>;
|
||||
export type AttributeMap = Required<CustomizableAttributeMap>;
|
||||
|
||||
export const samlConnectorConfigGuard = z
|
||||
.object({
|
||||
/**
|
||||
* This is the metadata of SAML service provider, automatically generated by `tenantId` and `ssoConnectorId`.
|
||||
* See details in {@link @logto/core/src/sso/SamlConnector/index.ts}.
|
||||
*/
|
||||
const samlServiceProviderMetadataGuard = z.object({
|
||||
entityId: z.string().min(1),
|
||||
assertionConsumerServiceUrl: z.string().min(1),
|
||||
});
|
||||
|
||||
export type SamlServiceProviderMetadata = z.infer<typeof samlServiceProviderMetadataGuard>;
|
||||
|
||||
/**
|
||||
* We only concern about the `entityId`, `signInEndpoint` and `x509Certificate` for now.
|
||||
*/
|
||||
export const samlIdentityProviderMetadataGuard = z.object({
|
||||
entityId: z.string(),
|
||||
signInEndpoint: z.string(),
|
||||
x509Certificate: z.string(),
|
||||
});
|
||||
|
||||
export type SamlIdentityProviderMetadata = z.infer<typeof samlIdentityProviderMetadataGuard>;
|
||||
|
||||
export const samlConnectorConfigGuard = samlIdentityProviderMetadataGuard
|
||||
.extend({
|
||||
attributeMapping: customizableAttributeMappingGuard,
|
||||
signInEndpoint: z.string(),
|
||||
entityId: z.string(),
|
||||
x509Certificate: z.string(),
|
||||
metadataUrl: z.string(),
|
||||
metadata: z.string(),
|
||||
metadataUrl: z.string(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
export type SamlConnectorConfig = z.infer<typeof samlConnectorConfigGuard>;
|
||||
|
||||
export const samlMetadataGuard = z
|
||||
.object({
|
||||
entityId: z.string(),
|
||||
nameIdFormat: z.string().array().optional(),
|
||||
signInEndpoint: z.string(),
|
||||
signingAlgorithm: z.string().optional(),
|
||||
x509Certificate: z.string(),
|
||||
})
|
||||
.catchall(jsonGuard); // Allow extra fields, also need to fit the `JsonObject` type.
|
||||
const samlMetadataGuard = z.object({
|
||||
serviceProvider: samlServiceProviderMetadataGuard,
|
||||
identityProvider: samlIdentityProviderMetadataGuard,
|
||||
});
|
||||
|
||||
export type SamlMetadata = z.infer<typeof samlMetadataGuard>;
|
||||
|
||||
export type SamlConfig = SamlConnectorConfig & SamlMetadata;
|
||||
|
||||
// Saml assertion returned user attribute value
|
||||
export const extendedSocialUserInfoGuard = socialUserInfoGuard.catchall(z.unknown());
|
||||
export type ExtendedSocialUserInfo = z.infer<typeof extendedSocialUserInfoGuard>;
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
patchSsoConnectorById,
|
||||
patchSsoConnectorConfigById,
|
||||
} from '#src/api/sso-connector.js';
|
||||
import { SsoProviderName } from '#src/constants.js';
|
||||
|
||||
describe('sso-connector library', () => {
|
||||
it('should return sso-connector-factories', async () => {
|
||||
|
@ -80,7 +81,7 @@ describe('post sso-connectors', () => {
|
|||
connectorName: 'test',
|
||||
config: {
|
||||
issuer: 23,
|
||||
entityId: 123,
|
||||
signInEndpoint: 123,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(HTTPError);
|
||||
|
@ -236,7 +237,7 @@ describe('patch sso-connector by id', () => {
|
|||
patchSsoConnectorById(id, {
|
||||
config: {
|
||||
issuer: 23,
|
||||
entityId: 123,
|
||||
signInEndpoint: 123,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(HTTPError);
|
||||
|
@ -264,6 +265,11 @@ describe('patch sso-connector by id', () => {
|
|||
expect(connector).toHaveProperty('config', config);
|
||||
expect(connector).toHaveProperty('syncProfile', true);
|
||||
|
||||
// Since we've provided a valid metadata content, check if the `providerConfig` is returned.
|
||||
if (providerName === SsoProviderName.SAML) {
|
||||
expect(connector.providerConfig).toBeDefined();
|
||||
}
|
||||
|
||||
await deleteSsoConnectorById(id);
|
||||
}
|
||||
);
|
||||
|
@ -287,7 +293,7 @@ describe('patch sso-connector config by id', () => {
|
|||
await expect(
|
||||
patchSsoConnectorConfigById(id, {
|
||||
issuer: 23,
|
||||
entityId: 123,
|
||||
signInEndpoint: 123,
|
||||
})
|
||||
).rejects.toThrow(HTTPError);
|
||||
|
||||
|
|
Loading…
Reference in a new issue