mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
chore(console,core): show invalid SSO config/metadata error message inline (#5053)
This commit is contained in:
parent
70b051bded
commit
c2c1a9e359
6 changed files with 109 additions and 19 deletions
|
@ -31,9 +31,12 @@ export type OidcGuideFormType = {
|
|||
scope?: string;
|
||||
};
|
||||
|
||||
export type GuideFormType<T extends SsoProviderName> = T extends SsoProviderName.OIDC
|
||||
export type GuideFormType<T extends SsoProviderName> = T extends
|
||||
| SsoProviderName.OIDC
|
||||
| SsoProviderName.GOOGLE_WORKSPACE
|
||||
| SsoProviderName.OKTA
|
||||
? OidcGuideFormType
|
||||
: T extends SsoProviderName.SAML
|
||||
: T extends SsoProviderName.SAML | SsoProviderName.AZURE_AD
|
||||
? SamlGuideFormType
|
||||
: never;
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { SsoProviderName } from '@logto/schemas';
|
||||
import { type Optional } from '@silverhand/essentials';
|
||||
import { SsoProviderName, type RequestErrorBody } from '@logto/schemas';
|
||||
import { conditional, type Optional } from '@silverhand/essentials';
|
||||
import cleanDeep from 'clean-deep';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useEffect } from 'react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { useForm, FormProvider, type Path } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
type ParsedSsoIdentityProviderConfig,
|
||||
type GuideFormType,
|
||||
type SsoConnectorConfig,
|
||||
type SamlGuideFormType,
|
||||
} from '@/pages/EnterpriseSso/types';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
|
@ -30,16 +32,21 @@ type Props<T extends SsoProviderName> = {
|
|||
onUpdated: (data: SsoConnectorWithProviderConfigWithGeneric<T>) => void;
|
||||
};
|
||||
|
||||
const invalidConfigErrorCode = 'connector.invalid_config';
|
||||
const invalidMetadataErrorCode = 'connector.invalid_metadata';
|
||||
|
||||
// This component contains only `data.config`.
|
||||
function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: Props<T>) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { id: ssoConnectorId, providerName, providerConfig, config } = data;
|
||||
|
||||
const api = useApi();
|
||||
const api = useApi({ hideErrorToast: true });
|
||||
|
||||
const methods = useForm<GuideFormType<T>>();
|
||||
|
||||
const {
|
||||
watch,
|
||||
setError,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit,
|
||||
reset,
|
||||
|
@ -55,6 +62,7 @@ function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: P
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedSsoConnector = await api
|
||||
// TODO: @darcyYe add console test case to remove attribute mapping config.
|
||||
.patch(`api/sso-connectors/${ssoConnectorId}`, {
|
||||
|
@ -66,6 +74,63 @@ function Connection<T extends SsoProviderName>({ isDeleted, data, onUpdated }: P
|
|||
onUpdated(updatedSsoConnector);
|
||||
|
||||
reset(updatedSsoConnector.config);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof HTTPError) {
|
||||
const { response } = error;
|
||||
const metadata = await response.clone().json<RequestErrorBody>();
|
||||
|
||||
// TODO: @darcyYe refactor the generic of `GuideFormType<T>`.
|
||||
// Typescript can not infer the generic `GuideFormType<T>`, find a better way to deal with the types later.
|
||||
|
||||
if (metadata.code === invalidConfigErrorCode) {
|
||||
// OIDC-based SSO connector's config only relies on the result of read from `issuer` field.
|
||||
if (
|
||||
[
|
||||
SsoProviderName.OIDC,
|
||||
SsoProviderName.GOOGLE_WORKSPACE,
|
||||
SsoProviderName.OKTA,
|
||||
].includes(providerName)
|
||||
) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setError('issuer' as Path<GuideFormType<T>>, {
|
||||
type: 'custom',
|
||||
message: metadata.message,
|
||||
});
|
||||
}
|
||||
|
||||
// OIDC-based config has been excluded in previous condition check.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const formConfig = watch() as SamlGuideFormType;
|
||||
const key =
|
||||
conditional(formConfig.metadata && 'metadata') ??
|
||||
conditional(formConfig.metadataUrl && 'metadataUrl');
|
||||
if (key) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setError(key as Path<GuideFormType<T>>, {
|
||||
type: 'custom',
|
||||
message: metadata.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid metadata error only happens for SAML based SSO connectors, when trying to init IdP with XML-format metadata.
|
||||
if (
|
||||
metadata.code === invalidMetadataErrorCode &&
|
||||
[SsoProviderName.SAML, SsoProviderName.AZURE_AD].includes(providerName)
|
||||
) {
|
||||
// Typescript can not infer the generic of setError() path.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const formConfig = watch() as SamlGuideFormType;
|
||||
const key =
|
||||
conditional(formConfig.metadata && 'metadata') ??
|
||||
conditional(formConfig.metadataUrl && 'metadataUrl');
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setError(key as Path<GuideFormType<T>>, { type: 'custom', message: metadata.message });
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"deepmerge": "^4.2.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"etag": "^1.8.1",
|
||||
"fast-xml-parser": "^4.2.5",
|
||||
"find-up": "^6.3.0",
|
||||
"got": "^13.0.0",
|
||||
"hash-wasm": "^4.9.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { type Optional } from '@silverhand/essentials';
|
||||
import { XMLValidator } from 'fast-xml-parser';
|
||||
import * as saml from 'samlify';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
@ -240,8 +241,24 @@ class SamlConnector {
|
|||
const idpMetadataXml = await this.getIdpMetadataXml();
|
||||
|
||||
if (idpMetadataXml) {
|
||||
// Samlify validator swallows the error, validate the XML metadata on our own.
|
||||
// Other appearance of SAML metadata validator is using '@authenio/samlify-node-xmllint',
|
||||
// but this validator failed to resolve a valid XML file. Align the use of validator later on.
|
||||
try {
|
||||
XMLValidator.validate(idpMetadataXml, {
|
||||
allowBooleanAttributes: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
throw new SsoConnectorError(SsoConnectorErrorCodes.InvalidMetadata, {
|
||||
message: SsoConnectorConfigErrorCodes.InvalidSamlXmlMetadata,
|
||||
metadata: idpMetadataXml,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
this._identityProvider =
|
||||
// eslint-disable-next-line new-cap
|
||||
this._identityProvider = saml.IdentityProvider({
|
||||
saml.IdentityProvider({
|
||||
metadata: idpMetadataXml,
|
||||
});
|
||||
return this._identityProvider;
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum SsoConnectorErrorCodes {
|
|||
}
|
||||
|
||||
export enum SsoConnectorConfigErrorCodes {
|
||||
InvalidSamlXmlMetadata = 'invalid_saml_xml_metadata',
|
||||
InvalidConfigResponse = 'invalid_config_response',
|
||||
FailToFetchConfig = 'fail_to_fetch_config',
|
||||
InvalidConnectorConfig = 'invalid_connector_config',
|
||||
|
|
|
@ -3214,6 +3214,9 @@ importers:
|
|||
etag:
|
||||
specifier: ^1.8.1
|
||||
version: 1.8.1
|
||||
fast-xml-parser:
|
||||
specifier: ^4.2.5
|
||||
version: 4.3.2
|
||||
find-up:
|
||||
specifier: ^6.3.0
|
||||
version: 6.3.0
|
||||
|
|
Loading…
Reference in a new issue