diff --git a/.changeset/ui-saml-idp-certificates.md b/.changeset/ui-saml-idp-certificates.md
new file mode 100644
index 00000000000..5a71c64118b
--- /dev/null
+++ b/.changeset/ui-saml-idp-certificates.md
@@ -0,0 +1,6 @@
+---
+'@clerk/ui': minor
+'@clerk/localizations': minor
+---
+
+The self-serve SSO configuration (`` enterprise connection page and the `ConfigureSSO` SAML steps) now shows every IdP signing certificate a SAML connection trusts, with its expiry, and lets an admin add certificates from a file (a PEM bundle adds several) or remove one, so an IdP key rotation no longer needs a replacement timed with the IdP. Saving sends the whole list and only when it changed. The list holds at most five certificates. New `configureSSOCertificateList*` and `configureSSOCertificatePrimaryBadge` appearance element descriptors cover the list, and new `configureSSO.signingCertificates.*` localization keys hold its strings.
diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts
index 51eb8049416..0665f4b68d0 100644
--- a/packages/localizations/src/ar-SA.ts
+++ b/packages/localizations/src/ar-SA.ts
@@ -897,6 +897,17 @@ export const arSA: LocalizationResource = {
title: 'اختر المزود',
warning: 'بمجرد اختيار المزود لا يمكنك التغيير مرة أخرى حتى انتهاء التكوين',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1335,6 +1346,10 @@ export const arSA: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts
index 2d3047732f7..e4ad0e6c004 100644
--- a/packages/localizations/src/be-BY.ts
+++ b/packages/localizations/src/be-BY.ts
@@ -898,6 +898,17 @@ export const beBY: LocalizationResource = {
title: 'Выберыце правайдэра',
warning: 'Пасля выбару правайдэра вы не зможаце змяніць яго, пакуль не скончыце канфігурацыю',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1338,6 +1349,10 @@ export const beBY: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts
index 97e0dea7866..717ee166f6e 100644
--- a/packages/localizations/src/bg-BG.ts
+++ b/packages/localizations/src/bg-BG.ts
@@ -899,6 +899,17 @@ export const bgBG: LocalizationResource = {
title: 'Изберете доставчик',
warning: 'След като изберете доставчик, не можете да го промените, докато конфигурацията не приключи',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1338,6 +1349,10 @@ export const bgBG: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts
index e4903dd4680..022665b1f17 100644
--- a/packages/localizations/src/bn-IN.ts
+++ b/packages/localizations/src/bn-IN.ts
@@ -904,6 +904,17 @@ export const bnIN: LocalizationResource = {
title: 'প্রদানকারী নির্বাচন করুন',
warning: 'একবার প্রদানকারী নির্বাচন করার পরে, কনফিগারেশন শেষ না হওয়া পর্যন্ত আপনি আবার পরিবর্তন করতে পারবেন না',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const bnIN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts
index 415dfb5651c..6fecadbf40b 100644
--- a/packages/localizations/src/ca-ES.ts
+++ b/packages/localizations/src/ca-ES.ts
@@ -905,6 +905,17 @@ export const caES: LocalizationResource = {
title: 'Seleccioneu un proveïdor',
warning: 'Un cop seleccionat un proveïdor no podreu canviar-lo fins que la configuració hagi finalitzat',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const caES: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts
index be7ddf037c6..2050a63cde1 100644
--- a/packages/localizations/src/cs-CZ.ts
+++ b/packages/localizations/src/cs-CZ.ts
@@ -902,6 +902,17 @@ export const csCZ: LocalizationResource = {
title: 'Vyberte poskytovatele',
warning: 'Jakmile vyberete poskytovatele, nelze ho změnit, dokud nebude konfigurace dokončena',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1343,6 +1354,10 @@ export const csCZ: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts
index e9b3b4facda..863123bccb2 100644
--- a/packages/localizations/src/da-DK.ts
+++ b/packages/localizations/src/da-DK.ts
@@ -897,6 +897,17 @@ export const daDK: LocalizationResource = {
title: 'Vælg udbyder',
warning: 'Når en udbyder er valgt, kan du ikke ændre den, før konfigurationen er færdig',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1336,6 +1347,10 @@ export const daDK: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts
index 85211a28986..3dae5675b44 100644
--- a/packages/localizations/src/de-DE.ts
+++ b/packages/localizations/src/de-DE.ts
@@ -906,6 +906,17 @@ export const deDE: LocalizationResource = {
warning:
'Sobald ein Anbieter ausgewählt ist, können Sie ihn nicht mehr ändern, bis die Konfiguration abgeschlossen ist',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1351,6 +1362,10 @@ export const deDE: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts
index 0467f20fb50..cbaa83a14eb 100644
--- a/packages/localizations/src/el-GR.ts
+++ b/packages/localizations/src/el-GR.ts
@@ -898,6 +898,17 @@ export const elGR: LocalizationResource = {
title: 'Επιλέξτε πάροχο',
warning: 'Μόλις επιλεγεί ένας πάροχος δεν μπορείτε να τον αλλάξετε μέχρι να ολοκληρωθεί η ρύθμιση',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1341,6 +1352,10 @@ export const elGR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts
index e028cff9e6f..8cf3fa19f17 100644
--- a/packages/localizations/src/en-GB.ts
+++ b/packages/localizations/src/en-GB.ts
@@ -897,6 +897,17 @@ export const enGB: LocalizationResource = {
title: 'Select provider',
warning: 'Once a provider is selected you cannot change again until the configuration is over',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const enGB: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index abbcb744c3f..34ba72cb85c 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -500,7 +500,7 @@ export const enUS: LocalizationResource = {
},
signingCertificate: {
fileUploaded: 'File uploaded',
- label: 'Signing certificate',
+ label: 'Signing certificates',
removeFile: 'Remove file',
replaceFile: 'Replace file',
uploadFile: 'Upload file',
@@ -583,7 +583,7 @@ export const enUS: LocalizationResource = {
},
signingCertificate: {
fileUploaded: 'File uploaded',
- label: 'Signing certificate',
+ label: 'Signing certificates',
removeFile: 'Remove file',
replaceFile: 'Replace file',
uploadFile: 'Upload file',
@@ -700,7 +700,7 @@ export const enUS: LocalizationResource = {
},
signingCertificate: {
fileUploaded: 'File uploaded',
- label: 'Signing certificate',
+ label: 'Signing certificates',
removeFile: 'Remove file',
replaceFile: 'Replace file',
uploadFile: 'Upload file',
@@ -822,7 +822,7 @@ export const enUS: LocalizationResource = {
},
signingCertificate: {
fileUploaded: 'File uploaded',
- label: 'Signing certificate',
+ label: 'Signing certificates',
removeFile: 'Remove file',
replaceFile: 'Replace file',
uploadFile: 'Upload file',
@@ -926,6 +926,17 @@ export const enUS: LocalizationResource = {
title: 'Select your identity provider',
warning: 'Once a provider is selected you cannot change again until the configuration is over',
},
+ signingCertificates: {
+ addCertificate: 'Add certificate',
+ expired: 'Expired {{date}}',
+ expires: 'Expires {{date}}',
+ expiryAfterSave: 'Expiry shows after you save.',
+ fileUnreadable: 'The file could not be read. Try again.',
+ notACertificate: 'One of the uploaded files is not a certificate.',
+ primary: 'Primary',
+ primaryTooltip: 'Checked first when verifying a sign-in. Every certificate in this list is trusted.',
+ removeCertificate: 'Remove certificate',
+ },
testConfigurationStep: {
error__noSuccessfulTestRun:
'You need at least one successful test run before you can continue. Generate a test URL and complete the sign-in flow.',
@@ -1375,6 +1386,10 @@ export const enUS: LocalizationResource = {
},
identityProvider: {
certificateExpires: 'Certificate expires',
+ certificates: 'Certificates',
+ certificatesCount: '{{count}} certificates',
+ certificatesSummary: '{{count}} certificates, earliest expires {{date}}',
+ certificatesSummaryExpired: '{{count}} certificates, one expired {{date}}',
clientSecret: {
placeholder: 'Leave empty to keep the current secret',
},
diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts
index f13e61b1055..881651ad605 100644
--- a/packages/localizations/src/es-CR.ts
+++ b/packages/localizations/src/es-CR.ts
@@ -898,6 +898,17 @@ export const esCR: LocalizationResource = {
title: 'Seleccionar proveedor',
warning: 'Una vez que se selecciona un proveedor no puedes cambiarlo hasta que termine la configuración',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1341,6 +1352,10 @@ export const esCR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts
index 175e88c7ef6..352b491767c 100644
--- a/packages/localizations/src/es-ES.ts
+++ b/packages/localizations/src/es-ES.ts
@@ -904,6 +904,17 @@ export const esES: LocalizationResource = {
title: 'Seleccionar proveedor',
warning: 'Una vez seleccionado un proveedor no podrás cambiarlo hasta que finalice la configuración',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1346,6 +1357,10 @@ export const esES: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts
index 35a9eab3747..fb8f136a774 100644
--- a/packages/localizations/src/es-MX.ts
+++ b/packages/localizations/src/es-MX.ts
@@ -899,6 +899,17 @@ export const esMX: LocalizationResource = {
title: 'Seleccionar proveedor',
warning: 'Una vez que se selecciona un proveedor no puedes cambiarlo hasta que termine la configuración',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1342,6 +1353,10 @@ export const esMX: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts
index 5f03cd55126..4bc80046ddc 100644
--- a/packages/localizations/src/es-UY.ts
+++ b/packages/localizations/src/es-UY.ts
@@ -898,6 +898,17 @@ export const esUY: LocalizationResource = {
title: 'Seleccionar proveedor',
warning: 'Una vez que se selecciona un proveedor no podés cambiarlo hasta que termine la configuración',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1340,6 +1351,10 @@ export const esUY: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts
index 72e0ee3f716..1cec93c532a 100644
--- a/packages/localizations/src/fa-IR.ts
+++ b/packages/localizations/src/fa-IR.ts
@@ -903,6 +903,17 @@ export const faIR: LocalizationResource = {
title: 'ارائهدهنده را انتخاب کنید',
warning: 'پس از انتخاب یک ارائهدهنده، نمیتوانید آن را تا پایان پیکربندی تغییر دهید',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const faIR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts
index c3b07cb926e..af968a9348d 100644
--- a/packages/localizations/src/fi-FI.ts
+++ b/packages/localizations/src/fi-FI.ts
@@ -904,6 +904,17 @@ export const fiFI: LocalizationResource = {
title: 'Valitse palveluntarjoaja',
warning: 'Kun palveluntarjoaja on valittu, et voi vaihtaa sitä ennen kuin määritys on valmis',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1346,6 +1357,10 @@ export const fiFI: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts
index 31685940074..f10b2f0e17e 100644
--- a/packages/localizations/src/fr-FR.ts
+++ b/packages/localizations/src/fr-FR.ts
@@ -907,6 +907,17 @@ export const frFR: LocalizationResource = {
warning:
"Une fois un fournisseur sélectionné, vous ne pourrez plus en changer jusqu'à la fin de la configuration",
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1351,6 +1362,10 @@ export const frFR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts
index 0c49ef326f3..9238c575468 100644
--- a/packages/localizations/src/he-IL.ts
+++ b/packages/localizations/src/he-IL.ts
@@ -897,6 +897,17 @@ export const heIL: LocalizationResource = {
title: 'בחר ספק',
warning: 'לאחר בחירת ספק לא ניתן לשנות אותו עד לסיום ההגדרה',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1332,6 +1343,10 @@ export const heIL: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts
index 3b2df157c4a..50faad7f25b 100644
--- a/packages/localizations/src/hi-IN.ts
+++ b/packages/localizations/src/hi-IN.ts
@@ -904,6 +904,17 @@ export const hiIN: LocalizationResource = {
title: 'प्रदाता चुनें',
warning: 'एक बार प्रदाता का चयन करने के बाद आप कॉन्फ़िगरेशन समाप्त होने तक इसे बदल नहीं सकते',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const hiIN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts
index 40c4423cc1c..d4f315fc89c 100644
--- a/packages/localizations/src/hr-HR.ts
+++ b/packages/localizations/src/hr-HR.ts
@@ -905,6 +905,17 @@ export const hrHR: LocalizationResource = {
title: 'Odaberite pružatelja',
warning: 'Nakon odabira pružatelja ne možete ga ponovno mijenjati dok konfiguracija ne završi',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1346,6 +1357,10 @@ export const hrHR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts
index bd0aeb00fc0..87f03437062 100644
--- a/packages/localizations/src/hu-HU.ts
+++ b/packages/localizations/src/hu-HU.ts
@@ -906,6 +906,17 @@ export const huHU: LocalizationResource = {
title: 'Szolgáltató kiválasztása',
warning: 'Miután kiválasztotta a szolgáltatót, nem módosíthatja, amíg a konfiguráció be nem fejeződik',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const huHU: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts
index bb8f7a58b70..4a56e4e5711 100644
--- a/packages/localizations/src/id-ID.ts
+++ b/packages/localizations/src/id-ID.ts
@@ -898,6 +898,17 @@ export const idID: LocalizationResource = {
title: 'Pilih penyedia',
warning: 'Setelah penyedia dipilih, Anda tidak dapat mengubahnya lagi sampai konfigurasi selesai',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1339,6 +1350,10 @@ export const idID: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts
index f53f4d6cff5..78dc41a170e 100644
--- a/packages/localizations/src/is-IS.ts
+++ b/packages/localizations/src/is-IS.ts
@@ -904,6 +904,17 @@ export const isIS: LocalizationResource = {
title: 'Veldu þjónustuaðila',
warning: 'Þegar þjónustuaðili hefur verið valinn er ekki hægt að breyta aftur fyrr en stillingu er lokið',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1346,6 +1357,10 @@ export const isIS: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts
index 826889a94b4..bb419bfd479 100644
--- a/packages/localizations/src/it-IT.ts
+++ b/packages/localizations/src/it-IT.ts
@@ -904,6 +904,17 @@ export const itIT: LocalizationResource = {
title: 'Seleziona provider',
warning: 'Una volta selezionato un provider non potrai cambiarlo fino al termine della configurazione',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1346,6 +1357,10 @@ export const itIT: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts
index c2cdb2926ad..4f1fe4d90ce 100644
--- a/packages/localizations/src/ja-JP.ts
+++ b/packages/localizations/src/ja-JP.ts
@@ -905,6 +905,17 @@ export const jaJP: LocalizationResource = {
title: 'プロバイダーを選択',
warning: 'プロバイダーを選択すると、設定が完了するまで変更できません',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const jaJP: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts
index 306bd58ce3c..f669637237f 100644
--- a/packages/localizations/src/kk-KZ.ts
+++ b/packages/localizations/src/kk-KZ.ts
@@ -898,6 +898,17 @@ export const kkKZ: LocalizationResource = {
title: 'Провайдерді таңдау',
warning: 'Провайдер таңдалғаннан кейін, конфигурация аяқталғанша өзгерте алмайсыз',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1332,6 +1343,10 @@ export const kkKZ: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts
index 8b76029443a..88e554eaefa 100644
--- a/packages/localizations/src/ko-KR.ts
+++ b/packages/localizations/src/ko-KR.ts
@@ -901,6 +901,17 @@ export const koKR: LocalizationResource = {
title: '공급자 선택',
warning: '공급자를 선택하면 구성이 완료될 때까지 다시 변경할 수 없습니다',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const koKR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts
index 375c45efd4a..6aa4840b5a8 100644
--- a/packages/localizations/src/mn-MN.ts
+++ b/packages/localizations/src/mn-MN.ts
@@ -898,6 +898,17 @@ export const mnMN: LocalizationResource = {
title: 'Үйлчилгээ үзүүлэгчийг сонгох',
warning: 'Үйлчилгээ үзүүлэгчийг сонгосны дараа тохиргоо дуустал өөрчлөх боломжгүй',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1339,6 +1350,10 @@ export const mnMN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts
index ed21fbaef48..7c53a9ae9c3 100644
--- a/packages/localizations/src/ms-MY.ts
+++ b/packages/localizations/src/ms-MY.ts
@@ -906,6 +906,17 @@ export const msMY: LocalizationResource = {
title: 'Pilih pembekal',
warning: 'Setelah pembekal dipilih anda tidak boleh menukar lagi sehingga konfigurasi selesai',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1348,6 +1359,10 @@ export const msMY: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts
index 9e42aac4b76..c31ca1ffa75 100644
--- a/packages/localizations/src/nb-NO.ts
+++ b/packages/localizations/src/nb-NO.ts
@@ -905,6 +905,17 @@ export const nbNO: LocalizationResource = {
title: 'Velg leverandør',
warning: 'Når en leverandør er valgt, kan du ikke endre igjen før konfigurasjonen er ferdig',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const nbNO: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts
index ddd60976415..69a5f136b18 100644
--- a/packages/localizations/src/nl-BE.ts
+++ b/packages/localizations/src/nl-BE.ts
@@ -898,6 +898,17 @@ export const nlBE: LocalizationResource = {
title: 'Provider selecteren',
warning: 'Zodra een provider is geselecteerd, kun je deze niet meer wijzigen totdat de configuratie is voltooid',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1339,6 +1350,10 @@ export const nlBE: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts
index e2a1969b441..27da0499ce9 100644
--- a/packages/localizations/src/nl-NL.ts
+++ b/packages/localizations/src/nl-NL.ts
@@ -898,6 +898,17 @@ export const nlNL: LocalizationResource = {
title: 'Provider selecteren',
warning: 'Zodra een provider is geselecteerd, kun je deze niet meer wijzigen totdat de configuratie is voltooid',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1339,6 +1350,10 @@ export const nlNL: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts
index 76d3adc7ea9..04af979284e 100644
--- a/packages/localizations/src/pl-PL.ts
+++ b/packages/localizations/src/pl-PL.ts
@@ -898,6 +898,17 @@ export const plPL: LocalizationResource = {
title: 'Wybierz dostawcę',
warning: 'Po wybraniu dostawcy nie można go ponownie zmienić aż do zakończenia konfiguracji',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const plPL: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts
index 0d9ac6023a7..99b368031c2 100644
--- a/packages/localizations/src/pt-BR.ts
+++ b/packages/localizations/src/pt-BR.ts
@@ -905,6 +905,17 @@ export const ptBR: LocalizationResource = {
warning:
'Depois que um provedor for selecionado, você não poderá alterá-lo até que a configuração seja concluída',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const ptBR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts
index 4440c052473..48d6a572489 100644
--- a/packages/localizations/src/pt-PT.ts
+++ b/packages/localizations/src/pt-PT.ts
@@ -906,6 +906,17 @@ export const ptPT: LocalizationResource = {
title: 'Selecionar fornecedor',
warning: 'Depois de um fornecedor ser selecionado não pode ser alterado até que a configuração esteja terminada',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const ptPT: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts
index b372cf79c68..be1e270ce09 100644
--- a/packages/localizations/src/ro-RO.ts
+++ b/packages/localizations/src/ro-RO.ts
@@ -905,6 +905,17 @@ export const roRO: LocalizationResource = {
title: 'Selectați furnizorul',
warning: 'Odată ce un furnizor este selectat, nu îl puteți schimba până când configurația nu este finalizată',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const roRO: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts
index 07d081dd992..4403998a770 100644
--- a/packages/localizations/src/ru-RU.ts
+++ b/packages/localizations/src/ru-RU.ts
@@ -898,6 +898,17 @@ export const ruRU: LocalizationResource = {
title: 'Выберите поставщика',
warning: 'После выбора поставщика вы не сможете изменить его до завершения настройки',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1341,6 +1352,10 @@ export const ruRU: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts
index fc9169d88d2..c73b35e563c 100644
--- a/packages/localizations/src/sk-SK.ts
+++ b/packages/localizations/src/sk-SK.ts
@@ -898,6 +898,17 @@ export const skSK: LocalizationResource = {
title: 'Vyberte poskytovateľa',
warning: 'Po výbere poskytovateľa ho nemôžete zmeniť, kým sa konfigurácia neukončí',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const skSK: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts
index cc7fbb83dfb..e43def61292 100644
--- a/packages/localizations/src/sr-RS.ts
+++ b/packages/localizations/src/sr-RS.ts
@@ -898,6 +898,17 @@ export const srRS: LocalizationResource = {
title: 'Izaberite provajdera',
warning: 'Kada se provajder izabere, ne možete ga ponovo menjati dok se konfiguracija ne završi',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const srRS: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts
index 4277bb57a62..31dfc362a9e 100644
--- a/packages/localizations/src/sv-SE.ts
+++ b/packages/localizations/src/sv-SE.ts
@@ -898,6 +898,17 @@ export const svSE: LocalizationResource = {
title: 'Välj leverantör',
warning: 'När en leverantör har valts kan du inte ändra igen förrän konfigurationen är klar',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const svSE: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts
index 8f8ee372d62..71ba727aadc 100644
--- a/packages/localizations/src/ta-IN.ts
+++ b/packages/localizations/src/ta-IN.ts
@@ -906,6 +906,17 @@ export const taIN: LocalizationResource = {
title: 'வழங்குநரைத் தேர்ந்தெடுக்கவும்',
warning: 'வழங்குநரைத் தேர்ந்தெடுத்த பிறகு, கட்டமைப்பு முடியும் வரை மீண்டும் மாற்ற முடியாது',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1350,6 +1361,10 @@ export const taIN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts
index 957f053751c..e5bf22ca4cd 100644
--- a/packages/localizations/src/te-IN.ts
+++ b/packages/localizations/src/te-IN.ts
@@ -905,6 +905,17 @@ export const teIN: LocalizationResource = {
title: 'ప్రొవైడర్ను ఎంచుకోండి',
warning: 'ఒకసారి ప్రొవైడర్ ఎంచుకున్న తర్వాత, కాన్ఫిగరేషన్ ముగిసే వరకు మీరు మళ్లీ మార్చలేరు',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1347,6 +1358,10 @@ export const teIN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts
index c98233dba9f..25185bfbb26 100644
--- a/packages/localizations/src/th-TH.ts
+++ b/packages/localizations/src/th-TH.ts
@@ -901,6 +901,17 @@ export const thTH: LocalizationResource = {
title: 'เลือกผู้ให้บริการ',
warning: 'เมื่อเลือกผู้ให้บริการแล้วคุณไม่สามารถเปลี่ยนได้อีกจนกว่าการกำหนดค่าจะเสร็จสิ้น',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1338,6 +1349,10 @@ export const thTH: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts
index fe69dcdc525..addbef60fef 100644
--- a/packages/localizations/src/tr-TR.ts
+++ b/packages/localizations/src/tr-TR.ts
@@ -898,6 +898,17 @@ export const trTR: LocalizationResource = {
title: 'Sağlayıcıyı seçin',
warning: 'Bir sağlayıcı seçildikten sonra yapılandırma bitene kadar tekrar değiştiremezsiniz',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const trTR: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts
index e3a16b8ea27..f714296df0c 100644
--- a/packages/localizations/src/uk-UA.ts
+++ b/packages/localizations/src/uk-UA.ts
@@ -898,6 +898,17 @@ export const ukUA: LocalizationResource = {
title: 'Виберіть постачальника',
warning: 'Після вибору постачальника ви не зможете змінити його, доки не буде завершено налаштування',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1337,6 +1348,10 @@ export const ukUA: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts
index 8c543c1e108..f4af25fadb9 100644
--- a/packages/localizations/src/vi-VN.ts
+++ b/packages/localizations/src/vi-VN.ts
@@ -904,6 +904,17 @@ export const viVN: LocalizationResource = {
title: 'Chọn nhà cung cấp',
warning: 'Khi đã chọn nhà cung cấp, bạn không thể thay đổi cho đến khi cấu hình hoàn tất',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1345,6 +1356,10 @@ export const viVN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts
index 2537e98bae0..429fba2d4d1 100644
--- a/packages/localizations/src/zh-CN.ts
+++ b/packages/localizations/src/zh-CN.ts
@@ -897,6 +897,17 @@ export const zhCN: LocalizationResource = {
title: '选择提供商',
warning: '选择提供商后,在配置完成之前无法再次更改',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1330,6 +1341,10 @@ export const zhCN: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts
index a8db934fa49..0a59569f066 100644
--- a/packages/localizations/src/zh-TW.ts
+++ b/packages/localizations/src/zh-TW.ts
@@ -900,6 +900,17 @@ export const zhTW: LocalizationResource = {
title: '選擇提供者',
warning: '選擇提供者後,在設定完成之前無法再次變更',
},
+ signingCertificates: {
+ addCertificate: undefined,
+ expired: undefined,
+ expires: undefined,
+ expiryAfterSave: undefined,
+ fileUnreadable: undefined,
+ notACertificate: undefined,
+ primary: undefined,
+ primaryTooltip: undefined,
+ removeCertificate: undefined,
+ },
testConfigurationStep: {
error__noSuccessfulTestRun: undefined,
subtitle: undefined,
@@ -1333,6 +1344,10 @@ export const zhTW: LocalizationResource = {
},
identityProvider: {
certificateExpires: undefined,
+ certificates: undefined,
+ certificatesCount: undefined,
+ certificatesSummary: undefined,
+ certificatesSummaryExpired: undefined,
clientSecret: {
placeholder: undefined,
},
diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts
index 588bd91a29f..912ac54e607 100644
--- a/packages/shared/src/types/localization.ts
+++ b/packages/shared/src/types/localization.ts
@@ -1302,6 +1302,10 @@ export type __internal_LocalizationResource = {
identityProvider: {
title: LocalizationValue;
certificateExpires: LocalizationValue;
+ certificates: LocalizationValue;
+ certificatesCount: LocalizationValue<'count'>;
+ certificatesSummary: LocalizationValue<'count' | 'date'>;
+ certificatesSummaryExpired: LocalizationValue<'count' | 'date'>;
editButton: LocalizationValue;
form: {
title: LocalizationValue;
@@ -2259,6 +2263,17 @@ export type __internal_LocalizationResource = {
};
};
};
+ signingCertificates: {
+ addCertificate: LocalizationValue;
+ expired: LocalizationValue<'date'>;
+ expires: LocalizationValue<'date'>;
+ expiryAfterSave: LocalizationValue;
+ fileUnreadable: LocalizationValue;
+ notACertificate: LocalizationValue;
+ primary: LocalizationValue;
+ primaryTooltip: LocalizationValue;
+ removeCertificate: LocalizationValue;
+ };
activate: {
title: LocalizationValue;
subtitle: LocalizationValue<'domain'>;
diff --git a/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts b/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts
new file mode 100644
index 00000000000..29ef2f5a37e
--- /dev/null
+++ b/packages/ui/src/components/ConfigureSSO/domain/__tests__/idpCertificates.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ addCertificates,
+ areCertificateBodies,
+ getIdpCertificateStatus,
+ haveCertificatesChanged,
+ parseCertificateFile,
+ removeCertificate,
+ toIdpCertificateEntries,
+ toIdpCertificatesParam,
+} from '../idpCertificates';
+
+const pem = (body: string) => `-----BEGIN CERTIFICATE-----\n${body}\n-----END CERTIFICATE-----\n`;
+
+const entry = (certificate: string, expiresAt: number | null = null) => ({
+ certificate,
+ issuedAt: null,
+ expiresAt,
+});
+
+describe('toIdpCertificateEntries', () => {
+ it('prefers the list over the single certificate', () => {
+ const list = [entry('a', 1), entry('b', 2)];
+ expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificates: list })).toBe(list);
+ });
+
+ it('falls back to the single certificate and its validity columns', () => {
+ expect(toIdpCertificateEntries({ idpCertificate: 'a' })).toEqual([entry('a')]);
+ expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificates: [] })).toEqual([entry('a')]);
+ expect(
+ toIdpCertificateEntries({ idpCertificate: 'a', idpCertificateIssuedAt: 1, idpCertificateExpiresAt: 2 }),
+ ).toEqual([{ certificate: 'a', issuedAt: 1, expiresAt: 2 }]);
+ expect(toIdpCertificateEntries({ idpCertificate: 'a', idpCertificateExpiresAt: 0 })).toEqual([entry('a')]);
+ });
+
+ it('is empty without a connection or certificate', () => {
+ expect(toIdpCertificateEntries(null)).toEqual([]);
+ expect(toIdpCertificateEntries({ idpCertificate: '' })).toEqual([]);
+ });
+});
+
+describe('parseCertificateFile', () => {
+ it('reads one PEM certificate', () => {
+ expect(parseCertificateFile(pem('AAAA\nBBBB'))).toEqual(['AAAABBBB']);
+ });
+
+ it('reads every block of a PEM bundle, ignoring text between blocks', () => {
+ expect(parseCertificateFile(`subject=CN=a\n${pem('AAAA')}subject=CN=b\r\n${pem('BBBB')}`)).toEqual([
+ 'AAAA',
+ 'BBBB',
+ ]);
+ });
+
+ it('reads a bare base64 certificate with whitespace', () => {
+ expect(parseCertificateFile(' AAAA\n BBBB\n')).toEqual(['AAAABBBB']);
+ });
+
+ it('is empty for an empty file', () => {
+ expect(parseCertificateFile(' ')).toEqual([]);
+ });
+});
+
+describe('areCertificateBodies', () => {
+ it('accepts base64 bodies and rejects anything else, such as a private key', () => {
+ expect(areCertificateBodies(['MIICozCCAYs=', 'AAAA'])).toBe(true);
+ expect(areCertificateBodies([])).toBe(false);
+ expect(
+ areCertificateBodies(parseCertificateFile('-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n')),
+ ).toBe(false);
+ });
+});
+
+describe('addCertificates', () => {
+ it('appends new certificates and skips ones already in the list', () => {
+ expect(addCertificates([entry('AAAA', 1)], ['AAAA', 'BBBB'])).toEqual([entry('AAAA', 1), entry('BBBB')]);
+ });
+
+ it('returns the same list when nothing is new', () => {
+ const entries = [entry('AAAA', 1)];
+ expect(addCertificates(entries, ['AAAA'])).toBe(entries);
+ });
+
+ it('adds a bundle in order without duplicates', () => {
+ expect(addCertificates([], ['AAAA', 'BBBB', 'AAAA'])).toEqual([entry('AAAA'), entry('BBBB')]);
+ });
+
+ it('never grows the list past the maximum', () => {
+ const four = ['A', 'B', 'C', 'D'].map(c => entry(c));
+ expect(addCertificates(four, ['E', 'F'])).toEqual([...four, entry('E')]);
+ expect(addCertificates([...four, entry('E')], ['F'])).toEqual([...four, entry('E')]);
+ });
+});
+
+describe('removeCertificate', () => {
+ it('drops the matching entry', () => {
+ expect(removeCertificate([entry('a'), entry('b')], 'a')).toEqual([entry('b')]);
+ });
+});
+
+describe('haveCertificatesChanged', () => {
+ const original = [entry('a', 1), entry('b', 2)];
+
+ it('is false for the same certificates in the same order', () => {
+ expect(haveCertificatesChanged([entry('a'), entry('b')], original)).toBe(false);
+ });
+
+ it('is true when one is added, removed, or reordered', () => {
+ expect(haveCertificatesChanged([entry('a'), entry('b'), entry('c')], original)).toBe(true);
+ expect(haveCertificatesChanged([entry('a')], original)).toBe(true);
+ expect(haveCertificatesChanged([entry('b'), entry('a')], original)).toBe(true);
+ });
+});
+
+describe('toIdpCertificatesParam', () => {
+ it('sends the certificate bodies in order', () => {
+ expect(toIdpCertificatesParam([entry('a'), entry('b')])).toEqual(['a', 'b']);
+ });
+});
+
+describe('getIdpCertificateStatus', () => {
+ const now = Date.UTC(2026, 8, 30);
+ const day = 24 * 60 * 60 * 1000;
+
+ it('classifies by the expiry date', () => {
+ expect(getIdpCertificateStatus(entry('a'), now)).toBe('unknown');
+ expect(getIdpCertificateStatus(entry('a', now - day), now)).toBe('expired');
+ expect(getIdpCertificateStatus(entry('a', now), now)).toBe('expired');
+ expect(getIdpCertificateStatus(entry('a', now + 10 * day), now)).toBe('expiring');
+ expect(getIdpCertificateStatus(entry('a', now + 31 * day), now)).toBe('valid');
+ });
+});
diff --git a/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts b/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts
new file mode 100644
index 00000000000..900f34106e5
--- /dev/null
+++ b/packages/ui/src/components/ConfigureSSO/domain/idpCertificates.ts
@@ -0,0 +1,108 @@
+import type { EnterpriseSamlConnectionNestedResource, EnterpriseSamlIdpCertificateResource } from '@clerk/shared/types';
+
+export type IdpCertificateEntry = EnterpriseSamlIdpCertificateResource;
+
+export type IdpCertificateStatus = 'expired' | 'expiring' | 'valid' | 'unknown';
+
+export const EXPIRY_WARNING_DAYS = 30;
+
+export const MAX_IDP_CERTIFICATES = 5;
+
+const PEM_HEADER = '-----BEGIN CERTIFICATE-----';
+const PEM_FOOTER = '-----END CERTIFICATE-----';
+const BASE64_BODY = /^[A-Za-z0-9+/]+=*$/;
+
+type SamlCertificateSource = Partial<
+ Pick<
+ EnterpriseSamlConnectionNestedResource,
+ 'idpCertificate' | 'idpCertificateIssuedAt' | 'idpCertificateExpiresAt' | 'idpCertificates'
+ >
+>;
+
+/**
+ * The certificates a connection trusts, primary first. A connection read
+ * without `idpCertificates` only carries the single certificate and its
+ * validity columns, which become one entry.
+ */
+export function toIdpCertificateEntries(saml: SamlCertificateSource | null | undefined): IdpCertificateEntry[] {
+ if (saml?.idpCertificates?.length) {
+ return saml.idpCertificates;
+ }
+ if (saml?.idpCertificate) {
+ return [
+ {
+ certificate: saml.idpCertificate,
+ issuedAt: saml.idpCertificateIssuedAt || null,
+ expiresAt: saml.idpCertificateExpiresAt || null,
+ },
+ ];
+ }
+ return [];
+}
+
+/**
+ * Splits an uploaded file into bare base64 certificate bodies: one per PEM
+ * block, or the whole file when it holds a single bare base64 certificate.
+ */
+export function parseCertificateFile(text: string): string[] {
+ const chunks = text.split(PEM_HEADER);
+ const bodies: string[] = [];
+ chunks.forEach((chunk, index) => {
+ if (index === 0 && chunks.length > 1) {
+ return;
+ }
+ const end = chunk.indexOf(PEM_FOOTER);
+ const body = (end >= 0 ? chunk.slice(0, end) : chunk).replace(/\s+/g, '');
+ if (body) {
+ bodies.push(body);
+ }
+ });
+ return bodies;
+}
+
+export function areCertificateBodies(bodies: string[]): boolean {
+ return bodies.length > 0 && bodies.every(body => BASE64_BODY.test(body));
+}
+
+export function addCertificates(entries: IdpCertificateEntry[], bodies: string[]): IdpCertificateEntry[] {
+ const known = new Set(entries.map(entry => entry.certificate));
+ const added: IdpCertificateEntry[] = [];
+ for (const certificate of bodies) {
+ if (entries.length + added.length >= MAX_IDP_CERTIFICATES) {
+ break;
+ }
+ if (!known.has(certificate)) {
+ known.add(certificate);
+ added.push({ certificate, issuedAt: null, expiresAt: null });
+ }
+ }
+ return added.length > 0 ? [...entries, ...added] : entries;
+}
+
+export function removeCertificate(entries: IdpCertificateEntry[], certificate: string): IdpCertificateEntry[] {
+ return entries.filter(entry => entry.certificate !== certificate);
+}
+
+export function haveCertificatesChanged(entries: IdpCertificateEntry[], original: IdpCertificateEntry[]): boolean {
+ return (
+ entries.length !== original.length ||
+ entries.some((entry, index) => entry.certificate !== original[index].certificate)
+ );
+}
+
+export function toIdpCertificatesParam(entries: IdpCertificateEntry[]): string[] {
+ return entries.map(entry => entry.certificate);
+}
+
+export function getIdpCertificateStatus(entry: IdpCertificateEntry, now: number = Date.now()): IdpCertificateStatus {
+ if (entry.expiresAt === null) {
+ return 'unknown';
+ }
+ if (entry.expiresAt <= now) {
+ return 'expired';
+ }
+ if (entry.expiresAt - now <= EXPIRY_WARNING_DAYS * 24 * 60 * 60 * 1000) {
+ return 'expiring';
+ }
+ return 'valid';
+}
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx
index b42c6be20e3..e3b30f0a4c0 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx
@@ -21,6 +21,7 @@ import { Checkmark, Clipboard } from '@/icons';
import { useFormControl } from '@/ui/utils/useFormControl';
import { useConfigureSSO } from '../../../ConfigureSSOContext';
+import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates';
import { Step } from '../../../elements/Step';
import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard';
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
@@ -326,10 +327,10 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
samlConnection?.idpCertificate ||
samlConnection?.idpMetadataUrl,
);
- const existingCertPresent = Boolean(samlConnection?.idpCertificate);
+ const initialCertificates = toIdpCertificateEntries(samlConnection);
const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl');
- const [certFile, setCertFile] = React.useState(null);
+ const [certificates, setCertificates] = React.useState(initialCertificates);
// Step-LOCAL submit state for the Continue button. `goNext` bubbles to the
// parent (this is the terminal nested step) and the parent DEFERS the
// configure→test advance until the updateConnection revalidate lands. Keeping
@@ -378,7 +379,7 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
const trimmedMetadataUrl = metadataUrlField.value.trim();
const trimmedSignOnUrl = signOnUrlField.value.trim();
const trimmedIssuer = issuerField.value.trim();
- const hasCert = certFile !== null || existingCertPresent;
+ const hasCert = certificates.length > 0;
const isValid =
mode === 'metadataUrl'
@@ -404,9 +405,9 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
signOnUrlField,
issuerField,
certificateField,
- certFile,
- onCertFileChange: setCertFile,
- existingCertPresent,
+ certificates,
+ onCertificatesChange: setCertificates,
+ initialCertificates,
},
labels: {
description: localizationKeys(
@@ -439,7 +440,7 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
const saml = await buildSamlConfigurationPayload({
mode,
metadataUrl: { value: metadataUrlField.value },
- manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
+ manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates },
});
await updateConnection(enterpriseConnection.id, { saml });
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx
index 901884d9238..5c1a74bda9e 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlGoogleConfigureSteps.tsx
@@ -9,6 +9,7 @@ import { localizationKeys } from '@/localization';
import { useFormControl } from '@/ui/utils/useFormControl';
import { useConfigureSSO } from '../../../ConfigureSSOContext';
+import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates';
import { Step } from '../../../elements/Step';
import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard';
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
@@ -158,12 +159,12 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => {
const hasExistingManualConfig = Boolean(
samlConnection?.idpSsoUrl || samlConnection?.idpEntityId || samlConnection?.idpCertificate,
);
- const existingCertPresent = Boolean(samlConnection?.idpCertificate);
+ const initialCertificates = toIdpCertificateEntries(samlConnection);
const existingMetadataPresent = Boolean(samlConnection?.idpMetadata);
const [mode, setMode] = React.useState(hasExistingManualConfig ? 'manual' : 'metadataFile');
const [metadataFile, setMetadataFile] = React.useState(null);
- const [certFile, setCertFile] = React.useState(null);
+ const [certificates, setCertificates] = React.useState(initialCertificates);
// Step-LOCAL submit state for the Continue button. `goNext` bubbles to the
// parent (this is the terminal nested step) and the parent DEFERS the
// configure→test advance until the updateConnection revalidate lands. Keeping
@@ -208,7 +209,7 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => {
const trimmedSignOnUrl = signOnUrlField.value.trim();
const trimmedIssuer = issuerField.value.trim();
- const hasCert = certFile !== null || existingCertPresent;
+ const hasCert = certificates.length > 0;
const hasMetadataFile = metadataFile !== null || existingMetadataPresent;
const isValid =
@@ -250,9 +251,9 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => {
signOnUrlField,
issuerField,
certificateField,
- certFile,
- onCertFileChange: setCertFile,
- existingCertPresent,
+ certificates,
+ onCertificatesChange: setCertificates,
+ initialCertificates,
},
labels: {
description: localizationKeys(
@@ -285,7 +286,7 @@ const SamlGoogleIdentityProviderMetadataStep = (): JSX.Element => {
const saml = await buildSamlConfigurationPayload({
mode,
metadataFile: { file: metadataFile },
- manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
+ manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates },
});
await updateConnection(enterpriseConnection.id, { saml });
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx
index a4e1bc94983..e82632fc3bc 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlMicrosoftConfigureSteps.tsx
@@ -30,6 +30,7 @@ import { truncateWithEndVisible } from '@/ui/utils/truncateTextWithEndVisible';
import { useFormControl } from '@/ui/utils/useFormControl';
import { useConfigureSSO } from '../../../ConfigureSSOContext';
+import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates';
import { Step } from '../../../elements/Step';
import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard';
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
@@ -654,10 +655,10 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => {
samlConnection?.idpCertificate ||
samlConnection?.idpMetadataUrl,
);
- const existingCertPresent = Boolean(samlConnection?.idpCertificate);
+ const initialCertificates = toIdpCertificateEntries(samlConnection);
const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl');
- const [certFile, setCertFile] = React.useState(null);
+ const [certificates, setCertificates] = React.useState(initialCertificates);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const metadataUrlField = useFormControl('idpMetadataUrl', samlConnection?.idpMetadataUrl ?? '', {
@@ -702,7 +703,7 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => {
const trimmedMetadataUrl = metadataUrlField.value.trim();
const trimmedSignOnUrl = signOnUrlField.value.trim();
const trimmedIssuer = issuerField.value.trim();
- const hasCert = certFile !== null || existingCertPresent;
+ const hasCert = certificates.length > 0;
const isValid =
mode === 'metadataUrl'
@@ -728,9 +729,9 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => {
signOnUrlField,
issuerField,
certificateField,
- certFile,
- onCertFileChange: setCertFile,
- existingCertPresent,
+ certificates,
+ onCertificatesChange: setCertificates,
+ initialCertificates,
},
labels: {
description: localizationKeys(
@@ -763,7 +764,7 @@ const SamlMicrosoftIdentityProviderMetadataStep = (): JSX.Element => {
const saml = await buildSamlConfigurationPayload({
mode,
metadataUrl: { value: metadataUrlField.value },
- manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
+ manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates },
});
await updateConnection(enterpriseConnection.id, { saml });
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx
index 39ca8214704..758303c80c4 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlOktaConfigureSteps.tsx
@@ -22,6 +22,7 @@ import { Checkmark, Clipboard } from '@/icons';
import { useFormControl } from '@/ui/utils/useFormControl';
import { useConfigureSSO } from '../../../ConfigureSSOContext';
+import { type IdpCertificateEntry, toIdpCertificateEntries } from '../../../domain/idpCertificates';
import { Step } from '../../../elements/Step';
import { useWizard, Wizard, type WizardStepConfig } from '../../../elements/Wizard';
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
@@ -512,10 +513,10 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => {
samlConnection?.idpCertificate ||
samlConnection?.idpMetadataUrl,
);
- const existingCertPresent = Boolean(samlConnection?.idpCertificate);
+ const initialCertificates = toIdpCertificateEntries(samlConnection);
const [mode, setMode] = React.useState(hasExistingConfig ? 'manual' : 'metadataUrl');
- const [certFile, setCertFile] = React.useState(null);
+ const [certificates, setCertificates] = React.useState(initialCertificates);
// Step-LOCAL submit state for the Continue button. `goNext` bubbles to the
// parent (this is the terminal nested step) and the parent DEFERS the
// configure→test advance until the updateConnection revalidate lands. Keeping
@@ -562,7 +563,7 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => {
const trimmedMetadataUrl = metadataUrlField.value.trim();
const trimmedSignOnUrl = signOnUrlField.value.trim();
const trimmedIssuer = issuerField.value.trim();
- const hasCert = certFile !== null || existingCertPresent;
+ const hasCert = certificates.length > 0;
const isValid =
mode === 'metadataUrl'
@@ -588,9 +589,9 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => {
signOnUrlField,
issuerField,
certificateField,
- certFile,
- onCertFileChange: setCertFile,
- existingCertPresent,
+ certificates,
+ onCertificatesChange: setCertificates,
+ initialCertificates,
},
labels: {
description: localizationKeys(
@@ -623,7 +624,7 @@ const SamlOktaIdentityProviderMetadataStep = (): JSX.Element => {
const saml = await buildSamlConfigurationPayload({
mode,
metadataUrl: { value: metadataUrlField.value },
- manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
+ manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates },
});
await updateConnection(enterpriseConnection.id, { saml });
// `goNext` bubbles to the parent, which DEFERS the advance to `test` until
diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx
index 0237bc7df53..7ce914627eb 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/shared/IdentityProviderConfigurationForm.tsx
@@ -11,16 +11,31 @@ import {
Flex,
Icon,
type LocalizationKey,
+ localizationKeys,
+ Span,
Text,
useLocalizations,
} from '@/customizables';
import type { useCardState } from '@/elements/contexts';
import { Field } from '@/elements/FieldControl';
import { Form } from '@/elements/Form';
-import { ArrowUpTray, Close } from '@/icons';
+import { Tooltip } from '@/elements/Tooltip';
+import { ArrowUpTray, Close, ExclamationTriangle } from '@/icons';
+import { formatDate } from '@/ui/utils/formatDate';
import type { FormControlState } from '@/ui/utils/useFormControl';
import { handleError } from '@/utils/errorHandler';
+import {
+ addCertificates,
+ areCertificateBodies,
+ getIdpCertificateStatus,
+ haveCertificatesChanged,
+ type IdpCertificateEntry,
+ MAX_IDP_CERTIFICATES,
+ parseCertificateFile,
+ removeCertificate,
+ toIdpCertificatesParam,
+} from '../../../../domain/idpCertificates';
import type { SamlIdpConfigurationMode } from '../../shared/IdentityProviderConfigurationModes';
type CardState = ReturnType;
@@ -56,9 +71,9 @@ type ManualConfigurationForm = {
signOnUrlField: FormControl;
issuerField: FormControl;
certificateField: FormControl;
- certFile: File | null;
- onCertFileChange: (file: File | null) => void;
- existingCertPresent?: boolean;
+ certificates: IdpCertificateEntry[];
+ onCertificatesChange: React.Dispatch>;
+ initialCertificates: IdpCertificateEntry[];
};
type ManualConfigurationLabels = {
@@ -158,13 +173,11 @@ const ManualPanel = ({ form, labels }: ManualPanelProps): JSX.Element => (
-
>
);
@@ -176,7 +189,8 @@ type BuildSamlPayloadParams = {
manual?: {
signOnUrl: string;
issuer: string;
- certFile: File | null;
+ certificates: IdpCertificateEntry[];
+ initialCertificates: IdpCertificateEntry[];
};
};
@@ -213,8 +227,8 @@ export const buildSamlConfigurationPayload = async ({
idpEntityId: manual.issuer.trim(),
};
- if (manual.certFile !== null) {
- payload.idpCertificate = await manual.certFile.text();
+ if (haveCertificatesChanged(manual.certificates, manual.initialCertificates)) {
+ payload.idpCertificates = toIdpCertificatesParam(manual.certificates);
}
return payload;
@@ -350,3 +364,217 @@ const FileUploadField = ({
);
};
+
+type CertificateListFieldProps = {
+ field: FormControl;
+ certificates: IdpCertificateEntry[];
+ onCertificatesChange: React.Dispatch>;
+ labels: FileUploadLabels;
+};
+
+const CERTIFICATE_FILE_TYPES = '.pem,.key,.crt,.cer,.cert';
+
+const CertificateListField = ({
+ field,
+ certificates,
+ onCertificatesChange,
+ labels,
+}: CertificateListFieldProps): JSX.Element => {
+ const { t } = useLocalizations();
+ const inputRef = React.useRef(null);
+ const canRemove = certificates.length > 1;
+ const canAdd = certificates.length < MAX_IDP_CERTIFICATES;
+
+ const onFileSelected = async (file: File | null): Promise => {
+ if (inputRef.current) {
+ inputRef.current.value = '';
+ }
+ if (!file) {
+ return;
+ }
+
+ let text: string;
+ try {
+ text = await file.text();
+ } catch {
+ field.setError(t(localizationKeys('configureSSO.signingCertificates.fileUnreadable')));
+ return;
+ }
+
+ const bodies = parseCertificateFile(text);
+ if (!areCertificateBodies(bodies)) {
+ field.setError(t(localizationKeys('configureSSO.signingCertificates.notACertificate')));
+ return;
+ }
+
+ field.clearFeedback();
+ onCertificatesChange(current => addCertificates(current, bodies));
+ };
+
+ return (
+
+
+
+
+
+
+
+ void onFileSelected(e.target.files?.[0] ?? null)}
+ />
+
+ {certificates.length > 0 && (
+
+ {certificates.map((entry, index) => (
+ ({
+ padding: theme.space.$2,
+ borderRadius: theme.radii.$md,
+ borderWidth: theme.borderWidths.$normal,
+ borderStyle: theme.borderStyles.$solid,
+ borderColor: theme.colors.$borderAlpha100,
+ })}
+ >
+
+
+
+ {entry.certificate}
+
+ {index === 0 && (
+
+
+ ({ display: 'inline-flex', borderRadius: theme.radii.$sm })}
+ >
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const CertificateExpiry = ({ entry }: { entry: IdpCertificateEntry }): JSX.Element => {
+ const status = getIdpCertificateStatus(entry);
+ const showsAlert = status === 'expired' || status === 'expiring';
+ const colorScheme = status === 'expired' ? 'danger' : status === 'expiring' ? 'warning' : 'secondary';
+
+ return (
+
+ {showsAlert && (
+
+ )}
+
+
+ );
+};
diff --git a/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx b/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx
index 44472584738..881522634ea 100644
--- a/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx
+++ b/packages/ui/src/components/OrganizationProfile/EnterpriseConnectionPage/IdentityProviderSection.tsx
@@ -15,6 +15,11 @@ import { handleError } from '@/utils/errorHandler';
import type { LocalizationKey } from '../../../customizables';
import { Col, localizationKeys, Text } from '../../../customizables';
+import {
+ getIdpCertificateStatus,
+ type IdpCertificateEntry,
+ toIdpCertificateEntries,
+} from '../../ConfigureSSO/domain/idpCertificates';
import { isOidcProvider } from '../../ConfigureSSO/domain/organizationEnterpriseConnection';
import type { EnterpriseConnectionMutations } from '../../ConfigureSSO/hooks/useOrganizationEnterpriseConnection';
import {
@@ -40,7 +45,13 @@ type IdentityProviderSectionProps = {
type FormScreenProps = IdentityProviderSectionProps & { onSuccess: () => void; onReset: () => void };
-type Detail = { id: string; label: LocalizationKey; value: string };
+type Detail = {
+ id: string;
+ label: LocalizationKey;
+ value?: string;
+ valueKey?: LocalizationKey;
+ tone?: 'danger' | 'warning';
+};
const SAML_MODES = ['metadataUrl', 'manual'] as const satisfies readonly SamlIdpConfigurationMode[];
const OIDC_MODES = ['discoveryUrl', 'manual'] as const satisfies readonly OidcIdpConfigurationMode[];
@@ -75,15 +86,56 @@ const samlDetails = (connection: EnterpriseConnectionResource): Detail[] => {
},
];
- if (saml && saml.idpCertificateExpiresAt > 0) {
- details.push({
+ const certificates = toIdpCertificateEntries(saml);
+ if (certificates.length > 0) {
+ details.push(certificatesDetail(certificates));
+ }
+
+ return details;
+};
+
+const toneFor = (entry: IdpCertificateEntry): Detail['tone'] => {
+ const status = getIdpCertificateStatus(entry);
+ return status === 'expired' ? 'danger' : status === 'expiring' ? 'warning' : undefined;
+};
+
+const certificatesDetail = (certificates: IdpCertificateEntry[]): Detail => {
+ const dated = certificates.filter(entry => entry.expiresAt !== null);
+ const earliest = dated.length > 0 ? dated.reduce((a, b) => ((b.expiresAt ?? 0) < (a.expiresAt ?? 0) ? b : a)) : null;
+
+ if (certificates.length === 1) {
+ const [only] = certificates;
+ return {
id: 'idpCertificateExpiresAt',
label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificateExpires'),
- value: formatDate(new Date(saml.idpCertificateExpiresAt)),
- });
+ value: only.expiresAt === null ? undefined : formatDate(new Date(only.expiresAt)),
+ tone: toneFor(only),
+ };
}
- return details;
+ const count = certificates.length;
+ if (!earliest || earliest.expiresAt === null) {
+ return {
+ id: 'idpCertificates',
+ label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificates'),
+ valueKey: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificatesCount', {
+ count,
+ }),
+ };
+ }
+
+ const expired = getIdpCertificateStatus(earliest) === 'expired';
+ return {
+ id: 'idpCertificates',
+ label: localizationKeys('organizationProfile.securityPage.connectionPage.identityProvider.certificates'),
+ valueKey: localizationKeys(
+ expired
+ ? 'organizationProfile.securityPage.connectionPage.identityProvider.certificatesSummaryExpired'
+ : 'organizationProfile.securityPage.connectionPage.identityProvider.certificatesSummary',
+ { count, date: formatDate(new Date(earliest.expiresAt)) },
+ ),
+ tone: toneFor(earliest),
+ };
};
const oidcDetails = (connection: EnterpriseConnectionResource): Detail[] => {
@@ -138,7 +190,7 @@ export const IdentityProviderSection = (props: IdentityProviderSectionProps): JS
({ gap: t.space.$3, minWidth: 0 })}>
{details
- .filter(detail => detail.value)
+ .filter(detail => detail.value || detail.valueKey)
.map(detail => (
- {detail.value}
+
+ {detail.value}
+
))}
@@ -203,10 +261,10 @@ const SamlForm = withCardStateProvider(
({ connection, updateConnection, onSuccess, onReset }: FormScreenProps): JSX.Element => {
const card = useCardState();
const saml = connection.samlConnection;
- const existingCertPresent = Boolean(saml?.idpCertificate);
+ const initialCertificates = toIdpCertificateEntries(saml);
const [mode, setMode] = useState(saml?.idpMetadataUrl ? 'metadataUrl' : 'manual');
- const [certFile, setCertFile] = useState(null);
+ const [certificates, setCertificates] = useState(initialCertificates);
const metadataUrlField = useFormControl('idpMetadataUrl', saml?.idpMetadataUrl ?? '', {
type: 'text',
@@ -248,9 +306,7 @@ const SamlForm = withCardStateProvider(
const isValid =
mode === 'metadataUrl'
? metadataUrlField.value.trim().length > 0
- : signOnUrlField.value.trim().length > 0 &&
- issuerField.value.trim().length > 0 &&
- (certFile !== null || existingCertPresent);
+ : signOnUrlField.value.trim().length > 0 && issuerField.value.trim().length > 0 && certificates.length > 0;
const formProps: IdentityProviderConfigurationFormProps =
mode === 'metadataUrl'
@@ -269,9 +325,9 @@ const SamlForm = withCardStateProvider(
signOnUrlField,
issuerField,
certificateField,
- certFile,
- onCertFileChange: setCertFile,
- existingCertPresent,
+ certificates,
+ onCertificatesChange: setCertificates,
+ initialCertificates,
},
labels: {
description: localizationKeys(
@@ -305,7 +361,7 @@ const SamlForm = withCardStateProvider(
const payload = await buildSamlConfigurationPayload({
mode,
metadataUrl: { value: metadataUrlField.value },
- manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
+ manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certificates, initialCertificates },
});
await updateConnection(connection.id, { saml: payload });
diff --git a/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx b/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx
index 004f1677179..d0295b1677e 100644
--- a/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx
+++ b/packages/ui/src/components/OrganizationProfile/__tests__/EnterpriseConnectionPage.test.tsx
@@ -10,6 +10,17 @@ import { EnterpriseConnectionPage } from '../EnterpriseConnectionPage';
const { createFixtures } = bindCreateFixtures('OrganizationProfile');
+// jsdom's File has no text(); the certificate upload reads the file with it.
+if (typeof File.prototype.text !== 'function') {
+ File.prototype.text = function (this: File) {
+ return new Promise(resolve => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result as string);
+ reader.readAsText(this);
+ });
+ };
+}
+
const withPageFixtures = (f: Parameters[0]>[0]) => {
f.withEnterpriseSso({ selfServeSSO: true });
f.withEmailAddress();
@@ -206,6 +217,138 @@ describe('EnterpriseConnectionPage', () => {
expect(await screen.findByText('Certificate expires')).toBeInTheDocument();
});
+ it('lists every trusted certificate in the Edit form, marking the primary', async () => {
+ const { wrapper, fixtures } = await createFixtures(withPageFixtures);
+ withNoTestRuns(fixtures);
+
+ const { userEvent, container } = renderPage(
+ wrapper,
+ fixtures,
+ samlConnectionWith({
+ idpCertificates: [
+ { certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') },
+ { certificate: 'CERTTWO', issuedAt: null, expiresAt: Date.parse('2020-05-01T00:00:00Z') },
+ ],
+ }),
+ );
+
+ await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2));
+ await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]);
+
+ const form = container.querySelector('.cl-actionCard') as HTMLElement;
+ expect(within(form).getByText('CERTONE')).toBeInTheDocument();
+ expect(within(form).getByText('CERTTWO')).toBeInTheDocument();
+ expect(within(form).getByText('Primary').closest('[tabindex="0"]')).not.toBeNull();
+ expect(within(form).getByText(/^Expired /)).toBeInTheDocument();
+ expect(within(form).getAllByRole('button', { name: 'Remove certificate' })).toHaveLength(2);
+ expect(within(form).getByRole('button', { name: 'Add certificate' })).toBeInTheDocument();
+ });
+
+ it('adds the certificates of an uploaded bundle and saves the whole list', async () => {
+ const { wrapper, fixtures } = await createFixtures(withPageFixtures);
+ withNoTestRuns(fixtures);
+ fixtures.clerk.organization?.updateEnterpriseConnection.mockResolvedValue(samlConnection());
+
+ const { userEvent, container } = renderPage(
+ wrapper,
+ fixtures,
+ samlConnectionWith({
+ idpCertificates: [{ certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') }],
+ }),
+ );
+
+ await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2));
+ await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]);
+
+ const form = container.querySelector('.cl-actionCard') as HTMLElement;
+ const bundle = new File(
+ [
+ '-----BEGIN CERTIFICATE-----\nCERTONE\n-----END CERTIFICATE-----\n' +
+ '-----BEGIN CERTIFICATE-----\nCERT\nTWO\n-----END CERTIFICATE-----\n',
+ ],
+ 'bundle.pem',
+ );
+ await userEvent.upload(form.querySelector('input[type="file"]') as HTMLInputElement, bundle);
+
+ expect(await within(form).findByText('CERTTWO')).toBeInTheDocument();
+ expect(within(form).getByText('Expiry shows after you save.')).toBeInTheDocument();
+
+ await userEvent.click(within(form).getByRole('button', { name: 'Save' }));
+
+ await waitFor(() => {
+ expect(fixtures.clerk.organization?.updateEnterpriseConnection).toHaveBeenCalledWith('ent_1', {
+ saml: {
+ idpSsoUrl: 'https://idp.example.com/sso',
+ idpEntityId: 'https://idp.example.com/entity',
+ idpCertificates: ['CERTONE', 'CERTTWO'],
+ },
+ });
+ });
+ });
+
+ it('removes a certificate but never the last one', async () => {
+ const { wrapper, fixtures } = await createFixtures(withPageFixtures);
+ withNoTestRuns(fixtures);
+
+ const { userEvent, container } = renderPage(
+ wrapper,
+ fixtures,
+ samlConnectionWith({
+ idpCertificates: [
+ { certificate: 'CERTONE', issuedAt: null, expiresAt: null },
+ { certificate: 'CERTTWO', issuedAt: null, expiresAt: null },
+ ],
+ }),
+ );
+
+ await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2));
+ await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]);
+
+ const form = container.querySelector('.cl-actionCard') as HTMLElement;
+ await userEvent.click(within(form).getAllByRole('button', { name: 'Remove certificate' })[1]);
+
+ expect(within(form).queryByText('CERTTWO')).not.toBeInTheDocument();
+ expect(within(form).getByRole('button', { name: 'Remove certificate' })).toBeDisabled();
+ });
+
+ it('rejects a file that is not a certificate', async () => {
+ const { wrapper, fixtures } = await createFixtures(withPageFixtures);
+ withNoTestRuns(fixtures);
+
+ const { userEvent, container } = renderPage(wrapper, fixtures, samlConnection());
+
+ await waitFor(() => expect(screen.getAllByRole('button', { name: 'Edit' })).toHaveLength(2));
+ await userEvent.click(screen.getAllByRole('button', { name: 'Edit' })[1]);
+
+ const form = container.querySelector('.cl-actionCard') as HTMLElement;
+ const key = new File(['-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n'], 'key.pem');
+ await userEvent.upload(form.querySelector('input[type="file"]') as HTMLInputElement, key);
+
+ expect(await within(form).findAllByText('One of the uploaded files is not a certificate.')).not.toHaveLength(0);
+ expect(within(form).getAllByRole('button', { name: 'Remove certificate' })).toHaveLength(1);
+ });
+
+ it('summarizes several certificates in the closed view', async () => {
+ const { wrapper, fixtures } = await createFixtures(withPageFixtures);
+ withNoTestRuns(fixtures);
+
+ renderPage(
+ wrapper,
+ fixtures,
+ samlConnectionWith({
+ idpCertificates: [
+ { certificate: 'CERTONE', issuedAt: null, expiresAt: Date.parse('2036-05-01T00:00:00Z') },
+ { certificate: 'CERTTWO', issuedAt: null, expiresAt: Date.parse('2030-05-01T00:00:00Z') },
+ { certificate: 'CERTTHREE', issuedAt: null, expiresAt: null },
+ ],
+ }),
+ );
+
+ expect(await screen.findByText('Certificates')).toBeInTheDocument();
+ expect(screen.getByText('3 certificates, earliest expires May 1, 2030')).toBeInTheDocument();
+ expect(screen.queryByText('Certificate expires')).not.toBeInTheDocument();
+ });
+
it('renders the OIDC variant of the service provider section and only the shared settings', async () => {
const { wrapper, fixtures } = await createFixtures(withPageFixtures);
withNoTestRuns(fixtures);
diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts
index bfad65bb647..e1ff3112d55 100644
--- a/packages/ui/src/customizables/elementDescriptors.ts
+++ b/packages/ui/src/customizables/elementDescriptors.ts
@@ -633,6 +633,12 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'configureSSOCertificateFileBadge',
'configureSSOCertificateFileName',
'configureSSOCertificateRemoveButton',
+ 'configureSSOCertificateList',
+ 'configureSSOCertificateListItem',
+ 'configureSSOCertificateListItemBody',
+ 'configureSSOCertificateListItemExpiry',
+ 'configureSSOCertificateListItemRemoveButton',
+ 'configureSSOCertificatePrimaryBadge',
'configureSSOTestUrlOpenButton',
'configureSSOTestRefreshButton',
diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts
index 5d1a89785ff..09fc4ffad1a 100644
--- a/packages/ui/src/internal/appearance.ts
+++ b/packages/ui/src/internal/appearance.ts
@@ -769,6 +769,12 @@ export type ElementsConfig = {
configureSSOCertificateFileBadge: WithOptions;
configureSSOCertificateFileName: WithOptions;
configureSSOCertificateRemoveButton: WithOptions;
+ configureSSOCertificateList: WithOptions;
+ configureSSOCertificateListItem: WithOptions;
+ configureSSOCertificateListItemBody: WithOptions;
+ configureSSOCertificateListItemExpiry: WithOptions;
+ configureSSOCertificateListItemRemoveButton: WithOptions;
+ configureSSOCertificatePrimaryBadge: WithOptions;
configureSSOTestUrlOpenButton: WithOptions;
configureSSOTestRefreshButton: WithOptions;