From 3bab5351a6b6bbb91ad8839951c540b18d2cc641 Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Mon, 4 Dec 2023 14:04:24 +0800 Subject: [PATCH] feat(console): add signing certificate reader (#5009) * feat(console): add signing certificate reader * fix(core): throw invalid certificate SsoConnectorError only when failed to construct a certificate --- .../console/src/pages/EnterpriseSso/types.ts | 4 +- .../index.module.scss | 0 .../{XmlFileReader => FileReader}/index.tsx | 63 ++++++++-------- .../ParsedConfigPreview/index.module.scss | 34 +++++---- .../ParsedConfigPreview/index.tsx | 65 +++++++++------- .../SamlMetadataForm/index.module.scss | 4 + .../Connection/SamlMetadataForm/index.tsx | 74 +++++++++++++++++-- .../Connection/SamlMetadataForm/utils.ts | 10 ++- .../EnterpriseSsoDetails/Connection/index.tsx | 8 +- .../middleware/koa-connector-error-handler.ts | 1 + packages/core/src/sso/SamlConnector/utils.ts | 40 ++++++---- packages/core/src/sso/types/error.ts | 10 +++ packages/core/src/sso/types/saml.ts | 4 +- .../src/locales/de/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/en/errors/connector.ts | 2 + .../src/locales/en/errors/single-sign-on.ts | 1 - .../admin-console/enterprise-sso-details.ts | 1 + .../admin-console/enterprise-sso.ts | 1 + .../src/locales/es/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/fr/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/it/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/ja/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/ko/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/pl-pl/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/pt-br/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/pt-pt/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/ru/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/tr-tr/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/zh-cn/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/zh-hk/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../src/locales/zh-tw/errors/connector.ts | 3 + .../admin-console/enterprise-sso-details.ts | 2 + .../admin-console/enterprise-sso.ts | 2 + .../toolkit/connector-kit/src/types/error.ts | 1 + 60 files changed, 311 insertions(+), 110 deletions(-) rename packages/console/src/pages/EnterpriseSsoDetails/Connection/{XmlFileReader => FileReader}/index.module.scss (100%) rename packages/console/src/pages/EnterpriseSsoDetails/Connection/{XmlFileReader => FileReader}/index.tsx (58%) diff --git a/packages/console/src/pages/EnterpriseSso/types.ts b/packages/console/src/pages/EnterpriseSso/types.ts index 41b38f0b5..626300288 100644 --- a/packages/console/src/pages/EnterpriseSso/types.ts +++ b/packages/console/src/pages/EnterpriseSso/types.ts @@ -63,8 +63,8 @@ export type ParsedSsoIdentityProviderConfig = entityId: string; signInEndpoint: string; x509Certificate: string; - expiresAt: number; - isValid: boolean; + certificateExpiresAt: number; + isCertificateValid: boolean; }; } : never; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/XmlFileReader/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Connection/FileReader/index.module.scss similarity index 100% rename from packages/console/src/pages/EnterpriseSsoDetails/Connection/XmlFileReader/index.module.scss rename to packages/console/src/pages/EnterpriseSsoDetails/Connection/FileReader/index.module.scss diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/XmlFileReader/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/FileReader/index.tsx similarity index 58% rename from packages/console/src/pages/EnterpriseSsoDetails/Connection/XmlFileReader/index.tsx rename to packages/console/src/pages/EnterpriseSsoDetails/Connection/FileReader/index.tsx index 34ddc518e..178fb7ffe 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/XmlFileReader/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/FileReader/index.tsx @@ -1,7 +1,8 @@ +import { type AdminConsoleKey } from '@logto/phrases'; import { Theme } from '@logto/schemas'; import { useCallback } from 'react'; -import { useDropzone, type FileRejection } from 'react-dropzone'; -import { useFormContext } from 'react-hook-form'; +import { useDropzone, type FileRejection, type Accept } from 'react-dropzone'; +import { type FieldError } from 'react-hook-form'; import Delete from '@/assets/icons/delete.svg'; import FileIconDark from '@/assets/icons/file-icon-dark.svg'; @@ -11,29 +12,29 @@ import Button from '@/ds-components/Button'; import IconButton from '@/ds-components/IconButton'; import useTheme from '@/hooks/use-theme'; -import { type SamlGuideFormType } from '../../../EnterpriseSso/types'; -import { calculateXmlFileSize } from '../SamlMetadataForm/utils'; +import { calculateFileSize } from '../SamlMetadataForm/utils'; import * as styles from './index.module.scss'; -const xmlMimeTypes = ['application/xml', 'text/xml']; -const xmlFileName = 'identity provider metadata.xml'; // Real file name does not matter, use a generic name. -const xmlFileSizeLimit = 500 * 1024; // 500 KB +const fileSizeLimit = 500 * 1024; // 500 KB -type Props = { - onChange: (xmlContent?: string) => void; +export type Props = { + onChange: (fileContent?: string) => void; value?: string; + attributes: { + accept: Accept; // File reader accepted file types. + buttonTitle: AdminConsoleKey; // I18n key for the button title. + defaultFilename: string; // Default file name. + defaultFileMimeType: string; // Default file MIME type when calculating the file size. + }; + fieldError?: FieldError; + setError: (error: FieldError) => void; }; -function XmlFileReader({ onChange, value }: Props) { +function FileReader({ onChange, value, attributes, fieldError, setError }: Props) { const theme = useTheme(); - const { - setError, - formState: { - errors: { metadata: metadataError }, - }, - } = useFormContext(); + const { accept, buttonTitle, defaultFilename, defaultFileMimeType } = attributes; /** * As you can see, per `useDropzone` hook's config, there are at most one file, if file is rejected, then we can return as long as we get the error message. @@ -43,7 +44,7 @@ function XmlFileReader({ onChange, value }: Props) { if (fileRejection.length > 0) { const fileErrors = fileRejection[0]?.errors; if (fileErrors?.[0]?.message) { - setError('metadata', { + setError({ type: 'custom', message: fileErrors[0]?.message, }); @@ -56,8 +57,8 @@ function XmlFileReader({ onChange, value }: Props) { return; } - const xmlContent = await acceptedFile.text(); - onChange(xmlContent); + const fileContent = await acceptedFile.text(); + onChange(fileContent); }, [onChange, setError] ); @@ -70,22 +71,22 @@ function XmlFileReader({ onChange, value }: Props) { onDrop, noDrag: true, // Only allow file selection via the file input. maxFiles: 1, - maxSize: xmlFileSizeLimit, + maxSize: fileSizeLimit, multiple: false, // Upload only one file at a time. - accept: Object.fromEntries(xmlMimeTypes.map((mimeType) => [mimeType, []])), + accept, }); return (
{value ? (
- {theme === Theme.Dark ? : } + {theme === Theme.Dark ? : }
- {xmlFileName} + {defaultFilename} {/* Not using `File.size` since the file content (variable `value` in this case) is stored in DB in string type */} - {`${(calculateXmlFileSize(value) / 1024).toFixed( - 2 - )} KB`} + {`${( + calculateFileSize(value, defaultFilename, defaultFileMimeType) / 1024 + ).toFixed(2)} KB`}
-
- {Boolean(metadataError) &&
{metadataError?.message}
} + {Boolean(fieldError) &&
{fieldError?.message}
} )}
); } -export default XmlFileReader; +export default FileReader; diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.module.scss index c306a59bb..8ef084c53 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.module.scss +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.module.scss @@ -19,9 +19,6 @@ font: var(--font-body-2); overflow-wrap: break-word; word-wrap: break-word; - display: flex; - flex-direction: row; - align-items: center; } } @@ -30,18 +27,25 @@ } } -.indicator { - width: 10px; - height: 10px; - margin-right: _.unit(2); - border-radius: 50%; - background: var(--color-on-success-container); -} +.certificatePreview { + display: flex; + flex-direction: row; + align-items: center; + font: var(--font-body-2); -.errorStatus { - background: var(--color-on-error-container); -} + .indicator { + width: 10px; + height: 10px; + margin-right: _.unit(2); + border-radius: 50%; + background: var(--color-on-success-container); + } -.copyToClipboard { - margin-left: _.unit(1); + .errorStatus { + background: var(--color-on-error-container); + } + + .copyToClipboard { + margin-left: _.unit(1); + } } diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx index 6b6117876..d87cb9682 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx @@ -15,51 +15,66 @@ type Props = { identityProviderConfig: ParsedSsoIdentityProviderConfig['identityProvider']; }; +type CertificatePreviewProps = { + identityProviderConfig: { + x509Certificate: string; + certificateExpiresAt: number; + isCertificateValid: boolean; + }; + className?: string; +}; + +export function CertificatePreview({ + identityProviderConfig: { x509Certificate, certificateExpiresAt, isCertificateValid }, + className, +}: CertificatePreviewProps) { + const { language } = i18next; + return ( +
+
+ + +
+ ); +} + function ParsedConfigPreview({ identityProviderConfig }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.enterprise_sso_details.saml_preview', }); - const { language } = i18next; if (!identityProviderConfig) { return null; } - const { entityId, signInEndpoint, x509Certificate, expiresAt, isValid } = identityProviderConfig; return (
{t('sign_on_url')}
-
{signInEndpoint}
+
{identityProviderConfig.signInEndpoint}
{t('entity_id')}
-
{entityId}
+
{identityProviderConfig.entityId}
{t('x509_certificate')}
-
- - +
diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.module.scss b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.module.scss index ff7d8279a..56e3fc508 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.module.scss +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.module.scss @@ -5,3 +5,7 @@ font: var(--font-body-2); margin-top: _.unit(0.5); } + +.certificatePreview { + margin-top: _.unit(1); +} diff --git a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.tsx b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.tsx index 835506525..be224324f 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/index.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next'; import FormField from '@/ds-components/FormField'; import InlineNotification from '@/ds-components/InlineNotification'; import TextInput from '@/ds-components/TextInput'; -import Textarea from '@/ds-components/Textarea'; import { type ParsedSsoIdentityProviderConfig, type SamlGuideFormType, @@ -14,9 +13,9 @@ import { } from '@/pages/EnterpriseSso/types.js'; import { uriValidator } from '@/utils/validator'; -import XmlFileReader from '../XmlFileReader'; +import FileReader, { type Props as FileReaderProps } from '../FileReader'; -import ParsedConfigPreview from './ParsedConfigPreview'; +import ParsedConfigPreview, { CertificatePreview } from './ParsedConfigPreview'; import SwitchFormatButton, { FormFormat } from './SwitchFormatButton'; import * as styles from './index.module.scss'; @@ -30,6 +29,30 @@ type SamlMetadataFormProps = { providerConfig?: ParsedSsoIdentityProviderConfig; }; +type KeyType = keyof Pick; // I.e. 'metadata' | 'x509Certificate'. +const keyToAttributes: Record = { + // Accept xml file. + metadata: { + buttonTitle: 'enterprise_sso.metadata.saml.metadata_xml_uploader_text', + accept: { + 'application/xml': [], + 'text/xml': [], + }, + defaultFilename: 'identity provider metadata.xml', + defaultFileMimeType: 'application/xml', + }, + x509Certificate: { + buttonTitle: 'enterprise_sso_details.upload_signing_certificate_button_text', + accept: { + 'application/x-x509-user-cert': ['.crt', '.cer'], + 'application/x-x509-ca-cert': ['.crt', '.cer'], + 'application/x-pem-file': ['.pem'], + }, + defaultFilename: 'signing certificate.cer', + defaultFileMimeType: 'application/x-x509-user-cert', + }, +}; + function SamlMetadataFormFields({ formFormat, identityProviderConfig, @@ -37,6 +60,7 @@ function SamlMetadataFormFields({ }: SamlMetadataFormFieldsProps) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { + setError, control, register, formState: { errors }, @@ -69,10 +93,36 @@ function SamlMetadataFormFields({ /> -