mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add SAML auth request handling endpoints
This commit is contained in:
parent
ebeca1607d
commit
10a310d50f
8 changed files with 466 additions and 20 deletions
|
@ -134,8 +134,12 @@ export const buildRouterObjects = <T extends UnknownRouter>(routers: T[], option
|
|||
// Filter out universal routes (mostly like a proxy route to withtyped)
|
||||
.filter(({ path }) => !path.includes('.*'))
|
||||
// TODO: Remove this and bring back `/saml-applications` routes before release.
|
||||
// Exclude `/saml-applications` routes for now.
|
||||
.filter(({ path }) => !path.startsWith('/saml-applications'))
|
||||
// Exclude `/saml-applications` routes and `/saml/:id/authn` for now.
|
||||
.filter(
|
||||
({ path }) =>
|
||||
!path.startsWith('/saml-applications') &&
|
||||
!(path.startsWith('/saml') && path.endsWith('/authn'))
|
||||
)
|
||||
.flatMap<RouteObject>(({ path: routerPath, stack, methods }) =>
|
||||
methods
|
||||
.map((method) => method.toLowerCase())
|
||||
|
|
|
@ -115,7 +115,9 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
|
|||
});
|
||||
};
|
||||
|
||||
const getSamlIdPMetadataByApplicationId = async (id: string): Promise<{ metadata: string }> => {
|
||||
const getSamlIdPMetadataByApplicationId = async (
|
||||
id: string
|
||||
): Promise<{ metadata: string; certificate: string }> => {
|
||||
const [{ tenantId }, { certificate }] = await Promise.all([
|
||||
findSamlApplicationConfigByApplicationId(id),
|
||||
findActiveSamlApplicationSecretByApplicationId(id),
|
||||
|
@ -132,11 +134,16 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
|
|||
Location: buildSingleSignOnUrl(tenantEndpoint, id),
|
||||
Binding: BindingType.Redirect,
|
||||
},
|
||||
{
|
||||
Location: buildSingleSignOnUrl(tenantEndpoint, id),
|
||||
Binding: BindingType.Post,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
metadata: idp.getMetadata(),
|
||||
certificate,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
82
packages/core/src/saml-applications/queries/index.ts
Normal file
82
packages/core/src/saml-applications/queries/index.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { type ToZodObject } from '@logto/connector-kit';
|
||||
import {
|
||||
SamlApplicationConfigs,
|
||||
SamlApplicationSecrets,
|
||||
Applications,
|
||||
ApplicationType,
|
||||
type Application,
|
||||
type SamlApplicationConfig,
|
||||
type SamlApplicationSecret,
|
||||
} from '@logto/schemas';
|
||||
import type { CommonQueryMethods } from '@silverhand/slonik';
|
||||
import { sql } from '@silverhand/slonik';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { convertToIdentifiers } from '#src/utils/sql.js';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Applications, true);
|
||||
const { table: samlApplicationConfigsTable, fields: samlApplicationConfigsFields } =
|
||||
convertToIdentifiers(SamlApplicationConfigs, true);
|
||||
const { table: samlApplicationSecretsTable, fields: samlApplicationSecretsFields } =
|
||||
convertToIdentifiers(SamlApplicationSecrets, true);
|
||||
|
||||
type NullableObject<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
[P in keyof T]: T[P] | null;
|
||||
};
|
||||
|
||||
type SamlApplicationSecretDetails = Pick<
|
||||
SamlApplicationSecret,
|
||||
'privateKey' | 'certificate' | 'active' | 'expiresAt'
|
||||
>;
|
||||
|
||||
export type SamlApplicationDetails = Pick<
|
||||
Application,
|
||||
'id' | 'secret' | 'name' | 'description' | 'customData' | 'oidcClientMetadata'
|
||||
> &
|
||||
Pick<SamlApplicationConfig, 'attributeMapping' | 'entityId' | 'acsUrl'> &
|
||||
NullableObject<SamlApplicationSecretDetails>;
|
||||
|
||||
const samlApplicationDetailsGuard = Applications.guard
|
||||
.pick({
|
||||
id: true,
|
||||
secret: true,
|
||||
name: true,
|
||||
description: true,
|
||||
customData: true,
|
||||
oidcClientMetadata: true,
|
||||
})
|
||||
.merge(
|
||||
SamlApplicationConfigs.guard.pick({
|
||||
attributeMapping: true,
|
||||
entityId: true,
|
||||
acsUrl: true,
|
||||
})
|
||||
)
|
||||
.merge(
|
||||
// Zod does not provide a way to convert all fields to nullable, so we need to do it manually. Other implementations seems can not make TypeScript happy.
|
||||
z.object({
|
||||
privateKey: SamlApplicationSecrets.guard.shape.privateKey.nullable(),
|
||||
certificate: SamlApplicationSecrets.guard.shape.certificate.nullable(),
|
||||
active: SamlApplicationSecrets.guard.shape.active.nullable(),
|
||||
expiresAt: SamlApplicationSecrets.guard.shape.expiresAt.nullable(),
|
||||
})
|
||||
) satisfies ToZodObject<SamlApplicationDetails>;
|
||||
|
||||
export const createSamlApplicationQueries = (pool: CommonQueryMethods) => {
|
||||
const getSamlApplicationDetailsById = async (id: string): Promise<SamlApplicationDetails> => {
|
||||
const result = await pool.one(sql`
|
||||
select ${fields.id} as id, ${fields.secret} as secret, ${fields.name} as name, ${fields.description} as description, ${fields.customData} as custom_data, ${fields.oidcClientMetadata} as oidc_client_metadata, ${samlApplicationConfigsFields.attributeMapping} as attribute_mapping, ${samlApplicationConfigsFields.entityId} as entity_id, ${samlApplicationConfigsFields.acsUrl} as acs_url, ${samlApplicationSecretsFields.privateKey} as private_key, ${samlApplicationSecretsFields.certificate} as certificate, ${samlApplicationSecretsFields.active} as active, ${samlApplicationSecretsFields.expiresAt} as expires_at
|
||||
from ${table}
|
||||
left join ${samlApplicationConfigsTable} on ${fields.id}=${samlApplicationConfigsFields.applicationId}
|
||||
left join ${samlApplicationSecretsTable} on ${fields.id}=${samlApplicationSecretsFields.applicationId}
|
||||
where ${fields.id}=${id} and ${fields.type}=${ApplicationType.SAML} and ${samlApplicationSecretsFields.active}=true
|
||||
`);
|
||||
|
||||
return samlApplicationDetailsGuard.parse(result);
|
||||
};
|
||||
|
||||
return {
|
||||
getSamlApplicationDetailsById,
|
||||
};
|
||||
};
|
|
@ -1,14 +1,30 @@
|
|||
import { generateStandardId, generateStandardShortId } from '@logto/shared';
|
||||
import { removeUndefinedKeys } from '@silverhand/essentials';
|
||||
import { addMinutes } from 'date-fns';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { AnonymousRouter, RouterInitArgs } from '#src/routes/types.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import {
|
||||
getSignInUrl,
|
||||
validateSamlApplicationDetails,
|
||||
loginRequestExtractGuard,
|
||||
getSamlIdpAndSp,
|
||||
} from './utils.js';
|
||||
|
||||
export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter>(
|
||||
...[router, { libraries }]: RouterInitArgs<T>
|
||||
...[router, { envSet, libraries, queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
samlApplications: { getSamlIdPMetadataByApplicationId },
|
||||
} = libraries;
|
||||
const {
|
||||
samlApplications: { getSamlApplicationDetailsById },
|
||||
samlApplicationSessions: { insertSession },
|
||||
} = queries;
|
||||
|
||||
router.get(
|
||||
'/saml-applications/:id/metadata',
|
||||
|
@ -29,4 +45,189 @@ export default function samlApplicationAnonymousRoutes<T extends AnonymousRouter
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// Redirect binding SAML authentication request endpoint
|
||||
router.get(
|
||||
'/saml/:id/authn',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
query: z
|
||||
.object({
|
||||
SAMLRequest: z.string().min(1),
|
||||
Signature: z.string().optional(),
|
||||
SigAlg: z.string().optional(),
|
||||
RelayState: z.string().optional(),
|
||||
})
|
||||
.catchall(z.string()),
|
||||
status: [200, 302, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
// TODO: handle `RelayState` later
|
||||
query: { Signature, ...rest },
|
||||
} = ctx.guard;
|
||||
|
||||
const [{ metadata, certificate }, details] = await Promise.all([
|
||||
getSamlIdPMetadataByApplicationId(id),
|
||||
getSamlApplicationDetailsById(id),
|
||||
]);
|
||||
|
||||
const { entityId, acsUrl, redirectUri } = validateSamlApplicationDetails(details);
|
||||
|
||||
const { idp, sp } = getSamlIdpAndSp({
|
||||
idp: { metadata, certificate },
|
||||
sp: { entityId, acsUrl },
|
||||
});
|
||||
|
||||
const octetString = Object.keys(ctx.request.query)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
.map((key) => key + '=' + encodeURIComponent(ctx.request.query[key] as string))
|
||||
.join('&');
|
||||
const { SAMLRequest, SigAlg } = rest;
|
||||
|
||||
// Parse login request
|
||||
try {
|
||||
const loginRequestResult = await idp.parseLoginRequest(sp, 'redirect', {
|
||||
query: removeUndefinedKeys({
|
||||
SAMLRequest,
|
||||
Signature,
|
||||
SigAlg,
|
||||
}),
|
||||
octetString,
|
||||
});
|
||||
|
||||
const extractResult = loginRequestExtractGuard.safeParse(loginRequestResult.extract);
|
||||
|
||||
if (!extractResult.success) {
|
||||
throw new RequestError({
|
||||
code: 'application.saml.invalid_saml_request',
|
||||
error: extractResult.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(
|
||||
extractResult.data.issuer === entityId,
|
||||
'application.saml.auth_request_issuer_not_match'
|
||||
);
|
||||
|
||||
const state = generateStandardId(32);
|
||||
const signInUrl = await getSignInUrl({
|
||||
issuer: envSet.oidc.issuer,
|
||||
applicationId: id,
|
||||
redirectUri,
|
||||
state,
|
||||
});
|
||||
|
||||
const currentDate = new Date();
|
||||
const expiresAt = addMinutes(currentDate, 10);
|
||||
const createSession = {
|
||||
id: generateStandardId(32),
|
||||
applicationId: id,
|
||||
oidcState: state,
|
||||
samlRequestId: extractResult.data.request.id,
|
||||
// Expire the session in 10 minutes.
|
||||
expiresAt: expiresAt.getTime(),
|
||||
};
|
||||
|
||||
const insertSamlAppSession = await insertSession(createSession);
|
||||
|
||||
ctx.redirect(signInUrl.toString());
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new RequestError({
|
||||
code: 'application.saml.invalid_saml_request',
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
// Post binding SAML authentication request endpoint
|
||||
router.post(
|
||||
'/saml/:id/authn',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
body: z.object({
|
||||
SAMLRequest: z.string().min(1),
|
||||
RelayState: z.string().optional(),
|
||||
}),
|
||||
status: [200, 302, 400, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
// TODO: handle `RelayState` later
|
||||
body: { SAMLRequest },
|
||||
} = ctx.guard;
|
||||
|
||||
const [{ metadata, certificate }, details] = await Promise.all([
|
||||
getSamlIdPMetadataByApplicationId(id),
|
||||
getSamlApplicationDetailsById(id),
|
||||
]);
|
||||
|
||||
const { acsUrl, entityId, redirectUri } = validateSamlApplicationDetails(details);
|
||||
|
||||
const { idp, sp } = getSamlIdpAndSp({
|
||||
idp: { metadata, certificate },
|
||||
sp: { entityId, acsUrl },
|
||||
});
|
||||
|
||||
// Parse login request
|
||||
try {
|
||||
const loginRequestResult = await idp.parseLoginRequest(sp, 'post', {
|
||||
body: {
|
||||
SAMLRequest,
|
||||
},
|
||||
});
|
||||
|
||||
const extractResult = loginRequestExtractGuard.safeParse(loginRequestResult.extract);
|
||||
|
||||
if (!extractResult.success) {
|
||||
throw new RequestError({
|
||||
code: 'application.saml.invalid_saml_request',
|
||||
error: extractResult.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(
|
||||
extractResult.data.issuer === entityId,
|
||||
'application.saml.auth_request_issuer_not_match'
|
||||
);
|
||||
|
||||
const state = generateStandardShortId();
|
||||
const signInUrl = await getSignInUrl({
|
||||
issuer: envSet.oidc.issuer,
|
||||
applicationId: id,
|
||||
redirectUri,
|
||||
state,
|
||||
});
|
||||
|
||||
const insertSamlAppSession = await insertSession({
|
||||
id: generateStandardId(),
|
||||
applicationId: id,
|
||||
oidcState: state,
|
||||
samlRequestId: extractResult.data.request.id,
|
||||
// Expire the session in 10 minutes.
|
||||
expiresAt: addMinutes(new Date(), 10).getTime(),
|
||||
});
|
||||
|
||||
ctx.redirect(signInUrl.toString());
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof RequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new RequestError({
|
||||
code: 'application.saml.invalid_saml_request',
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
148
packages/core/src/saml-applications/routes/utils.ts
Normal file
148
packages/core/src/saml-applications/routes/utils.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
import { Prompt, QueryKey, ReservedScope, UserScope } from '@logto/js';
|
||||
import { type SamlAcsUrl } from '@logto/schemas';
|
||||
import { deduplicate, tryThat } from '@silverhand/essentials';
|
||||
import { XMLValidator } from 'fast-xml-parser';
|
||||
import saml from 'samlify';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { fetchOidcConfigRaw } from '#src/sso/OidcConnector/utils.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import { type SamlApplicationDetails } from '../queries/index.js';
|
||||
|
||||
const getAuthorizationEndpoint = async (issuer: string): Promise<string> => {
|
||||
const { authorizationEndpoint } = await tryThat(
|
||||
async () => fetchOidcConfigRaw(issuer),
|
||||
(error) => {
|
||||
if (error instanceof ZodError) {
|
||||
throw new RequestError({
|
||||
code: 'oidc.invalid_request',
|
||||
message: error.message,
|
||||
error: error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
return authorizationEndpoint;
|
||||
};
|
||||
|
||||
export const getSignInUrl = async ({
|
||||
issuer,
|
||||
applicationId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
}: {
|
||||
issuer: string;
|
||||
applicationId: string;
|
||||
redirectUri: string;
|
||||
scope?: string;
|
||||
state?: string;
|
||||
}) => {
|
||||
const authorizationEndpoint = await getAuthorizationEndpoint(issuer);
|
||||
|
||||
const queryParameters = new URLSearchParams({
|
||||
[QueryKey.ClientId]: applicationId,
|
||||
[QueryKey.RedirectUri]: redirectUri,
|
||||
[QueryKey.ResponseType]: 'code',
|
||||
[QueryKey.Prompt]: Prompt.Login,
|
||||
});
|
||||
|
||||
// TODO: get value of `scope` parameters according to setup in attribute mapping.
|
||||
queryParameters.append(
|
||||
QueryKey.Scope,
|
||||
// For security reasons, DO NOT include the offline_access scope by default.
|
||||
deduplicate([
|
||||
ReservedScope.OpenId,
|
||||
UserScope.Profile,
|
||||
UserScope.Roles,
|
||||
UserScope.Organizations,
|
||||
UserScope.OrganizationRoles,
|
||||
UserScope.CustomData,
|
||||
UserScope.Identities,
|
||||
...(scope?.split(' ') ?? []),
|
||||
]).join(' ')
|
||||
);
|
||||
|
||||
if (state) {
|
||||
queryParameters.append(QueryKey.State, state);
|
||||
}
|
||||
|
||||
return new URL(`${authorizationEndpoint}?${queryParameters.toString()}`);
|
||||
};
|
||||
|
||||
export const validateSamlApplicationDetails = (details: SamlApplicationDetails) => {
|
||||
const {
|
||||
entityId,
|
||||
acsUrl,
|
||||
oidcClientMetadata: { redirectUris },
|
||||
} = details;
|
||||
|
||||
assertThat(acsUrl, 'application.saml.acs_url_required');
|
||||
assertThat(entityId, 'application.saml.entity_id_required');
|
||||
assertThat(redirectUris[0], 'oidc.invalid_redirect_uri');
|
||||
|
||||
return {
|
||||
entityId,
|
||||
acsUrl,
|
||||
redirectUri: redirectUris[0],
|
||||
};
|
||||
};
|
||||
|
||||
export const getSamlIdpAndSp = ({
|
||||
idp: { metadata, certificate },
|
||||
sp: { entityId, acsUrl },
|
||||
}: {
|
||||
idp: { metadata: string; certificate: string };
|
||||
sp: { entityId: string; acsUrl: SamlAcsUrl };
|
||||
}): { idp: saml.IdentityProviderInstance; sp: saml.ServiceProviderInstance } => {
|
||||
// eslint-disable-next-line new-cap
|
||||
const idp = saml.IdentityProvider({
|
||||
metadata,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const sp = saml.ServiceProvider({
|
||||
entityID: entityId,
|
||||
assertionConsumerService: [
|
||||
{
|
||||
Binding: acsUrl.binding,
|
||||
Location: acsUrl.url,
|
||||
},
|
||||
],
|
||||
signingCert: certificate,
|
||||
authnRequestsSigned: idp.entityMeta.isWantAuthnRequestsSigned(),
|
||||
allowCreate: false,
|
||||
});
|
||||
|
||||
// Used to check whether xml content is valid in format.
|
||||
saml.setSchemaValidator({
|
||||
validate: async (xmlContent: string) => {
|
||||
try {
|
||||
XMLValidator.validate(xmlContent, {
|
||||
allowBooleanAttributes: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { idp, sp };
|
||||
};
|
||||
|
||||
export const loginRequestExtractGuard = z.object({
|
||||
issuer: z.string(),
|
||||
request: z.object({
|
||||
id: z.string(),
|
||||
destination: z.string(),
|
||||
issueInstant: z.string(),
|
||||
assertionConsumerServiceUrl: z.string(),
|
||||
}),
|
||||
});
|
|
@ -3,7 +3,7 @@ import { assert } from '@silverhand/essentials';
|
|||
import camelcaseKeys, { type CamelCaseKeys } from 'camelcase-keys';
|
||||
import { got, HTTPError } from 'got';
|
||||
import { createRemoteJWKSet, jwtVerify, type JWTVerifyOptions } from 'jose';
|
||||
import { z } from 'zod';
|
||||
import { z, ZodError } from 'zod';
|
||||
|
||||
import {
|
||||
SsoConnectorConfigErrorCodes,
|
||||
|
@ -20,30 +20,28 @@ import {
|
|||
type OidcTokenResponse,
|
||||
} from '../types/oidc.js';
|
||||
|
||||
export const fetchOidcConfig = async (
|
||||
issuer: string
|
||||
): Promise<CamelCaseKeys<OidcConfigResponse>> => {
|
||||
try {
|
||||
export const fetchOidcConfigRaw = async (issuer: string) => {
|
||||
const { body } = await got.get(`${issuer}/.well-known/openid-configuration`, {
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
const result = oidcConfigResponseGuard.safeParse(body);
|
||||
return camelcaseKeys(oidcConfigResponseGuard.parse(body));
|
||||
};
|
||||
|
||||
if (!result.success) {
|
||||
export const fetchOidcConfig = async (
|
||||
issuer: string
|
||||
): Promise<CamelCaseKeys<OidcConfigResponse>> => {
|
||||
try {
|
||||
return await fetchOidcConfigRaw(issuer);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ZodError) {
|
||||
throw new SsoConnectorError(SsoConnectorErrorCodes.InvalidConfig, {
|
||||
config: { issuer },
|
||||
message: SsoConnectorConfigErrorCodes.InvalidConfigResponse,
|
||||
error: result.error.flatten(),
|
||||
error: error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
return camelcaseKeys(result.data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SsoConnectorError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new SsoConnectorError(SsoConnectorErrorCodes.InvalidConfig, {
|
||||
config: { issuer },
|
||||
message: SsoConnectorConfigErrorCodes.FailToFetchConfig,
|
||||
|
|
|
@ -29,6 +29,7 @@ import { createUserQueries } from '#src/queries/user.js';
|
|||
import { createUsersRolesQueries } from '#src/queries/users-roles.js';
|
||||
import { createVerificationStatusQueries } from '#src/queries/verification-status.js';
|
||||
import { createSamlApplicationConfigQueries } from '#src/saml-applications/queries/configs.js';
|
||||
import { createSamlApplicationQueries } from '#src/saml-applications/queries/index.js';
|
||||
import { createSamlApplicationSecretsQueries } from '#src/saml-applications/queries/secrets.js';
|
||||
import { createSamlApplicationSessionQueries } from '#src/saml-applications/queries/sessions.js';
|
||||
|
||||
|
@ -66,6 +67,7 @@ export default class Queries {
|
|||
samlApplicationSecrets = createSamlApplicationSecretsQueries(this.pool);
|
||||
samlApplicationConfigs = createSamlApplicationConfigQueries(this.pool);
|
||||
samlApplicationSessions = createSamlApplicationSessionQueries(this.pool);
|
||||
samlApplications = createSamlApplicationQueries(this.pool);
|
||||
personalAccessTokens = new PersonalAccessTokensQueries(this.pool);
|
||||
verificationRecords = new VerificationRecordQueries(this.pool);
|
||||
accountCenters = new AccountCenterQueries(this.pool);
|
||||
|
|
|
@ -27,8 +27,12 @@ const application = {
|
|||
'Only HTTP-POST binding is supported for receiving SAML assertions.',
|
||||
can_not_delete_active_secret: 'Can not delete the active secret.',
|
||||
no_active_secret: 'No active secret found.',
|
||||
entity_id_required: 'Entity ID is required to generate metadata.',
|
||||
entity_id_required: 'Entity ID is required.',
|
||||
invalid_certificate_pem_format: 'Invalid PEM certificate format',
|
||||
acs_url_required: 'Assertion Consumer Service URL is required.',
|
||||
invalid_saml_request: 'Invalid SAML authentication request.',
|
||||
auth_request_issuer_not_match:
|
||||
'The issuer of the SAML authentication request mismatch with service provider entity ID.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue