0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

feat(console): update certificate display (#5007)

This commit is contained in:
Darcy Ye 2023-11-30 11:11:26 +08:00 committed by GitHub
parent 6e4dd0432d
commit 949708b0f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 143 additions and 15 deletions

View file

@ -63,6 +63,8 @@ export type ParsedSsoIdentityProviderConfig<T extends SsoProviderName> =
entityId: string;
signInEndpoint: string;
x509Certificate: string;
expiresAt: number;
isValid: boolean;
};
}
: never;

View file

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

View file

@ -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 (
<div className={styles.container}>
<div>
@ -31,7 +38,29 @@ function ParsedConfigPreview({ identityProviderConfig }: Props) {
</div>
<div>
<div className={styles.title}>{t('x509_certificate')}</div>
<div className={styles.content}>{x509Certificate}</div>
<div className={styles.content}>
<div className={classNames(styles.indicator, !isValid && styles.errorStatus)} />
<DynamicT
forKey="enterprise_sso_details.saml_preview.certificate_content"
interpolation={{
date: new Date(expiresAt).toLocaleDateString(
// TODO: check if Logto's language tags are compatible.
conditional(isLanguageTag(language) && language) ?? 'en',
{
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}
),
}}
/>
<CopyToClipboard
className={styles.copyToClipboard}
variant="icon"
value={x509Certificate}
/>
</div>
</div>
</div>
);

View file

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

View file

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

View file

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

View file

@ -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<typeof samlIdentityProviderMetadataGuard>;
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(),
}),
]);

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

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

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */

View file

@ -110,6 +110,8 @@ const enterprise_sso_details = {
entity_id: 'Issuer',
/** UNTRANSLATED */
x509_certificate: 'Signing certificate',
/** UNTRANSLATED */
certificate_content: 'Expiring {{date}}',
},
oidc_preview: {
/** UNTRANSLATED */