0
Fork 0
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:
Darcy Ye 2023-12-04 16:47:11 +08:00 committed by GitHub
parent 70b051bded
commit c2c1a9e359
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 19 deletions

View file

@ -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;

View file

@ -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;
}
})
);

View file

@ -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",

View file

@ -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;

View file

@ -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',

View file

@ -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