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:
parent
6e4dd0432d
commit
949708b0f9
22 changed files with 143 additions and 15 deletions
|
@ -63,6 +63,8 @@ export type ParsedSsoIdentityProviderConfig<T extends SsoProviderName> =
|
|||
entityId: string;
|
||||
signInEndpoint: string;
|
||||
x509Certificate: string;
|
||||
expiresAt: number;
|
||||
isValid: boolean;
|
||||
};
|
||||
}
|
||||
: never;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
]);
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -110,6 +110,8 @@ const enterprise_sso_details = {
|
|||
entity_id: 'Issuer',
|
||||
/** UNTRANSLATED */
|
||||
x509_certificate: 'Signing certificate',
|
||||
/** UNTRANSLATED */
|
||||
certificate_content: 'Expiring {{date}}',
|
||||
},
|
||||
oidc_preview: {
|
||||
/** UNTRANSLATED */
|
||||
|
|
Loading…
Add table
Reference in a new issue