diff --git a/packages/console/src/pages/EnterpriseSso/types.ts b/packages/console/src/pages/EnterpriseSso/types.ts index 7dc6315fa..41b38f0b5 100644 --- a/packages/console/src/pages/EnterpriseSso/types.ts +++ b/packages/console/src/pages/EnterpriseSso/types.ts @@ -63,6 +63,8 @@ export type ParsedSsoIdentityProviderConfig = entityId: string; signInEndpoint: string; x509Certificate: string; + expiresAt: number; + isValid: boolean; }; } : never; 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 7eeddee0d..c306a59bb 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,6 +19,9 @@ font: var(--font-body-2); overflow-wrap: break-word; word-wrap: break-word; + display: flex; + flex-direction: row; + align-items: center; } } @@ -26,3 +29,19 @@ margin-bottom: _.unit(6); } } + +.indicator { + width: 10px; + height: 10px; + margin-right: _.unit(2); + border-radius: 50%; + background: var(--color-on-success-container); +} + +.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 6dff9b8b9..6b6117876 100644 --- a/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx +++ b/packages/console/src/pages/EnterpriseSsoDetails/Connection/SamlMetadataForm/ParsedConfigPreview/index.tsx @@ -1,6 +1,12 @@ +import { isLanguageTag } from '@logto/language-kit'; import { type SsoProviderName } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; +import classNames from 'classnames'; +import i18next from 'i18next'; import { useTranslation } from 'react-i18next'; +import CopyToClipboard from '@/ds-components/CopyToClipboard'; +import DynamicT from '@/ds-components/DynamicT'; import { type ParsedSsoIdentityProviderConfig } from '@/pages/EnterpriseSso/types.js'; import * as styles from './index.module.scss'; @@ -13,12 +19,13 @@ 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 } = identityProviderConfig; + const { entityId, signInEndpoint, x509Certificate, expiresAt, isValid } = identityProviderConfig; return (
@@ -31,7 +38,29 @@ function ParsedConfigPreview({ identityProviderConfig }: Props) {
{t('x509_certificate')}
-
{x509Certificate}
+
+
+ + +
); diff --git a/packages/core/src/sso/SamlConnector/index.ts b/packages/core/src/sso/SamlConnector/index.ts index 54ca1c093..99fb47036 100644 --- a/packages/core/src/sso/SamlConnector/index.ts +++ b/packages/core/src/sso/SamlConnector/index.ts @@ -14,7 +14,7 @@ import { type ExtendedSocialUserInfo, type SamlServiceProviderMetadata, type SamlIdentityProviderMetadata, - samlIdentityProviderMetadataGuard, + manualSamlConnectorConfigGuard, } from '../types/saml.js'; import { @@ -213,7 +213,7 @@ class SamlConnector { */ private getIdpMetadataJson() { // Required fields of metadata should not be undefined. - const result = samlIdentityProviderMetadataGuard.safeParse(this.idpConfig); + const result = manualSamlConnectorConfigGuard.safeParse(this.idpConfig); if (!result.success) { throw new SsoConnectorError(SsoConnectorErrorCodes.InvalidConfig, { diff --git a/packages/core/src/sso/SamlConnector/utils.test.ts b/packages/core/src/sso/SamlConnector/utils.test.ts index 9a546c63d..6a5b85f9b 100644 --- a/packages/core/src/sso/SamlConnector/utils.test.ts +++ b/packages/core/src/sso/SamlConnector/utils.test.ts @@ -1,4 +1,8 @@ -import { attributeMappingPostProcessor, getExtendedUserInfoFromRawUserProfile } from './utils.js'; +import { + attributeMappingPostProcessor, + getExtendedUserInfoFromRawUserProfile, + getPemCertificate, +} from './utils.js'; const expectedDefaultAttributeMapping = { id: 'nameID', @@ -44,3 +48,12 @@ describe('getExtendedUserInfoFromRawUserProfile', () => { }); }); }); + +const testCertificate = + '-----BEGIN CERTIFICATE-----\nMIIC8DCCAdigAwIBAgIQcu5YCNUIL6JNotFNdirF2DANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yMzEwMjUwODAyMDNaFw0yNjEwMjUwODAyMDNaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQgU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2EC5TZmW2ePPI0Od2Z3qFykouY/R8SBVJDD9xUcAIocMSqMLsxqd9ydkjaNC+QLbBUnpCvUd7+7ZyVcABbr5ixIMU+yxKIoZQdchECyasrR4HHXHXMeijQ8ziyF3Ys1yRB+iVQd2wZI+26pXlq9/bmT/keqMqdbAFD78QAYVF0LniL+sQav9Y0tsgrqXaE0GzqpTUsUfEcc1kynIQQG4ltFAkMTqaDhgw44S1GErjYC91dPEZMj4Ywwf1FIfnNJaRZoG77F3SlWUg345z/kAHBzNKjFMq3deobCHDZCZBJ6a+ABzgqdunUo4xBFG/YHNjjGkZEImALwp+P45mF5OLQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAK7s967KnFm0d7R1HpTHhr6D+L/X2Ejmgawo2HlkFLsHXPgGkeogrXl0Fw6NImJ+Zo/ChE2Vb8ZeYoEz5mdAYc0hK4k4UWJkv3yZ0GPKOzEcIWZ8Q8WAKqqWnzaO8NmZKpdc/sk8PluKH/BJ7IjEHZUgzhmuRGuJGJhVn2EPLXFIxBubyRlyMhBEZvX4syeeiCwGzvZY9CoTUPqftlrvc1xs78GFN+8cT2+B0vjcbifMkZ1Hq0iPQLN/LotM1qGbSVu/OFhuA+8mnp3Acw3XNZPOy9dZdNiVBF8ZoUz0rAC64dKYROPEDJhBTF30UzDcq6lfLA9KAgzEzupAxB8D4N\n-----END CERTIFICATE-----'; + +describe('getPemCertificate', () => { + it('should not throw error with a valid certificate', () => { + expect(() => getPemCertificate(testCertificate)).not.toThrow(); + }); +}); diff --git a/packages/core/src/sso/SamlConnector/utils.ts b/packages/core/src/sso/SamlConnector/utils.ts index 432f7eebf..98246bfd5 100644 --- a/packages/core/src/sso/SamlConnector/utils.ts +++ b/packages/core/src/sso/SamlConnector/utils.ts @@ -1,3 +1,5 @@ +import { X509Certificate } from 'node:crypto'; + import * as validator from '@authenio/samlify-node-xmllint'; import { type Optional, appendPath } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials'; @@ -52,23 +54,32 @@ export const parseXmlMetadata = ( entityId: idP.entityMeta.getEntityID(), // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment signInEndpoint: singleSignOnService, - // 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 = samlIdentityProviderMetadataGuard.safeParse(rawSamlMetadata); + // 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 + const rawX509Certificate: string = idP.entityMeta.getX509Certificate( + saml.Constants.wording.certUse.signing + ); - if (!result.success) { + try { + const certificate = getPemCertificate(rawX509Certificate); + const expiresAt = new Date(certificate.validTo).getTime(); + + // The return type of `samlify` + return samlIdentityProviderMetadataGuard.parse({ + ...rawSamlMetadata, + expiresAt, + isValid: expiresAt > Date.now(), + x509Certificate: certificate.toJSON(), // This returns the parsed certificate in string-type. + }); + } catch (error: unknown) { throw new SsoConnectorError(SsoConnectorErrorCodes.InvalidMetadata, { message: SsoConnectorConfigErrorCodes.InvalidConnectorConfig, metadata: rawSamlMetadata, - error: result.error, + error, }); } - - return result.data; }; /** @@ -222,3 +233,20 @@ export const buildSpEntityId = (baseUrl: URL, connectorId: string) => { */ export const buildAssertionConsumerServiceUrl = (baseUrl: URL, ssoConnectorId: string) => appendPath(baseUrl, `api/authn/${ssoPath}/saml/${ssoConnectorId}`).toString(); + +const pemCertificatePrefix = '-----BEGIN CERTIFICATE-----'; +const pemCertificateSuffix = '-----END CERTIFICATE-----'; + +const withPemCertificateWrapper = (certificateContent: string) => + `${pemCertificatePrefix}\n${certificateContent}\n${pemCertificateSuffix}`; + +const isPemCertificateWithWrapper = (certificateContent: string) => + certificateContent.startsWith(`${pemCertificatePrefix}\n`) && + certificateContent.endsWith(`\n${pemCertificateSuffix}`); + +export const getPemCertificate = (certificateContent: string) => { + const rawCertificate = isPemCertificateWithWrapper(certificateContent) + ? certificateContent + : withPemCertificateWrapper(certificateContent); + return new X509Certificate(rawCertificate); +}; diff --git a/packages/core/src/sso/types/saml.ts b/packages/core/src/sso/types/saml.ts index 024ced4b0..a713785ff 100644 --- a/packages/core/src/sso/types/saml.ts +++ b/packages/core/src/sso/types/saml.ts @@ -40,9 +40,17 @@ export const samlIdentityProviderMetadataGuard = z.object({ entityId: z.string(), signInEndpoint: z.string(), x509Certificate: z.string(), + expiresAt: z.number(), // Timestamp in milliseconds. + isValid: z.boolean(), }); export type SamlIdentityProviderMetadata = z.infer; +export const manualSamlConnectorConfigGuard = samlIdentityProviderMetadataGuard.pick({ + entityId: true, + signInEndpoint: true, + x509Certificate: true, +}); + export const samlConnectorConfigGuard = z.union([ // Config using Metadata URL z.object({ @@ -55,7 +63,7 @@ export const samlConnectorConfigGuard = z.union([ attributeMapping: customizableAttributeMapGuard.optional(), }), // Config using Metadata detail - samlIdentityProviderMetadataGuard.extend({ + manualSamlConnectorConfigGuard.extend({ attributeMapping: customizableAttributeMapGuard.optional(), }), ]); diff --git a/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts index 5b62bbc59..b43228730 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/enterprise-sso-details.ts @@ -61,6 +61,7 @@ const enterprise_sso_details = { sign_on_url: 'Sign on URL', entity_id: 'Issuer', x509_certificate: 'Signing certificate', + certificate_content: 'Expiring {{date}}', }, oidc_preview: { authorization_endpoint: 'Authorization endpoint', diff --git a/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/es/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/it/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ja/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pl-pl/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/ru/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-hk/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */ diff --git a/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts b/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts index 997aeaf43..1a49ddeed 100644 --- a/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts +++ b/packages/phrases/src/locales/zh-tw/translation/admin-console/enterprise-sso-details.ts @@ -110,6 +110,8 @@ const enterprise_sso_details = { entity_id: 'Issuer', /** UNTRANSLATED */ x509_certificate: 'Signing certificate', + /** UNTRANSLATED */ + certificate_content: 'Expiring {{date}}', }, oidc_preview: { /** UNTRANSLATED */