diff --git a/.changeset/reverification-enterprise-sso.md b/.changeset/reverification-enterprise-sso.md new file mode 100644 index 00000000000..71d0c07c13d --- /dev/null +++ b/.changeset/reverification-enterprise-sso.md @@ -0,0 +1,8 @@ +--- +'@clerk/ui': minor +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/localizations': patch +--- + +Offer enterprise SSO as a first factor in the reverification modal. Users with an `enterprise_sso` factor now get a button per enterprise connection, verify with their identity provider in a popup, and the action that asked for reverification resumes without a page reload. When the popup is blocked the modal falls back to a full-page redirect, and `oauthFlow: 'redirect'` on `useReverification` or the modal props opts into the redirect from the start. `session.prepareFirstFactorVerification` accepts `actionCompleteRedirectUrl` for the `enterprise_sso` strategy. diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index b57ed02a8fc..50fb45f3213 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -280,6 +280,7 @@ export class Session extends BaseResource implements SessionResource { emailAddressId: factor.emailAddressId, enterpriseConnectionId: factor.enterpriseConnectionId, redirectUrl: factor.redirectUrl, + actionCompleteRedirectUrl: factor.actionCompleteRedirectUrl, } as EnterpriseSSOConfig; break; default: diff --git a/packages/clerk-js/src/utils/authenticateWithPopup.ts b/packages/clerk-js/src/utils/authenticateWithPopup.ts index 9f107868f13..2ee854d8a5b 100644 --- a/packages/clerk-js/src/utils/authenticateWithPopup.ts +++ b/packages/clerk-js/src/utils/authenticateWithPopup.ts @@ -1,4 +1,5 @@ import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; +import { buildPopupCallbackUrls } from '@clerk/shared/internal/clerk-js/popupCallback'; import type { AuthenticateWithPopupParams, AuthenticateWithRedirectParams } from '@clerk/shared/types'; import type { Clerk } from '../core/clerk'; @@ -19,8 +20,6 @@ export async function _authenticateWithPopup( return; } - const accountPortalHost = buildAccountsBaseUrl(client.frontendApi); - const { redirectUrl } = params; // We set the force_redirect_url query parameter to ensure that the user is redirected to the correct page even @@ -32,10 +31,11 @@ export async function _authenticateWithPopup( // All URLs are decorated with the dev browser token in development mode since we're moving between AP and the app. const redirectUrlWithForceRedirectUrl = client.buildUrlWithAuth(r.toString()); - const popupRedirectUrlComplete = client.buildUrlWithAuth(`${accountPortalHost}/popup-callback`); - const popupRedirectUrl = client.buildUrlWithAuth( - `${accountPortalHost}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`, - ); + const { + accountsOrigin: accountPortalHost, + redirectUrl: popupRedirectUrl, + redirectUrlComplete: popupRedirectUrlComplete, + } = buildPopupCallbackUrls(client.frontendApi, url => client.buildUrlWithAuth(url), redirectUrlWithForceRedirectUrl); const messageHandler = async (event: MessageEvent) => { if (event.origin !== accountPortalHost) { @@ -101,8 +101,6 @@ export function wrapWithPopupRoutes( redirectUrl: string; }, ): { redirectCallbackUrl: string; redirectUrl: string } { - const accountPortalHost = buildAccountsBaseUrl(client.frontendApi); - // We set the force_redirect_url query parameter to ensure that the user is redirected to the correct page even // in situations like a modal transfer flow. const r = new URL(redirectCallbackUrl); @@ -111,9 +109,10 @@ export function wrapWithPopupRoutes( // All URLs are decorated with the dev browser token in development mode since we're moving between AP and the app. const redirectUrlWithForceRedirectUrl = client.buildUrlWithAuth(r.toString()); - const popupRedirectUrlComplete = client.buildUrlWithAuth(`${accountPortalHost}/popup-callback`); - const popupRedirectUrl = client.buildUrlWithAuth( - `${accountPortalHost}/popup-callback?return_url=${encodeURIComponent(redirectUrlWithForceRedirectUrl)}`, + const { redirectUrl: popupRedirectUrl, redirectUrlComplete: popupRedirectUrlComplete } = buildPopupCallbackUrls( + client.frontendApi, + url => client.buildUrlWithAuth(url), + redirectUrlWithForceRedirectUrl, ); return { redirectCallbackUrl: popupRedirectUrl, redirectUrl: popupRedirectUrlComplete }; diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index c707911a13b..6df3cffad77 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -1593,6 +1593,7 @@ export const arSA: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1615,6 +1616,10 @@ export const arSA: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index db7b9ca5650..e7af532c1f4 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -1600,6 +1600,7 @@ export const beBY: LocalizationResource = { actionText: 'Паспрабуйце іншы метад для верыфікацыі.', blockButton__backupCode: 'Увядзіце код з рэзервовага кода', blockButton__emailCode: 'Увядзіце код, адправлены на электронную пошту', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Увядзіце пароль', blockButton__phoneCode: 'Увядзіце код, адправлены на тэлефон', @@ -1622,6 +1623,10 @@ export const beBY: LocalizationResource = { subtitle: 'Калі ласка, праверце вашу электронную пошту для кода.', title: 'Код з email', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Няма даступных метадаў для верыфікацыі.', subtitle: 'Калі ласка, выберыце іншы метад або звярніцеся ў службу падтрымкі.', diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index fc88d5a762e..a9b8d90e4ac 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -1597,6 +1597,7 @@ export const bgBG: LocalizationResource = { actionText: "Don't have one of these?", blockButton__backupCode: 'Use backup code', blockButton__emailCode: 'Send code to email', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Login with password', blockButton__phoneCode: 'Send code to phone', @@ -1619,6 +1620,10 @@ export const bgBG: LocalizationResource = { subtitle: "We've sent a code to your email.", title: 'Email verification', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Unable to proceed. No available authentication methods.', subtitle: 'Something went wrong.', diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 7385f976b99..f7c4d3b3732 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -1603,6 +1603,7 @@ export const bnIN: LocalizationResource = { actionText: 'এর কোনটিই নেই?', blockButton__backupCode: 'একটি ব্যাকআপ কোড ব্যবহার করুন', blockButton__emailCode: '{{identifier}}-এ ইমেইল কোড পাঠান', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'আপনার পাসকি ব্যবহার করুন', blockButton__password: 'আপনার পাসওয়ার্ড দিয়ে চালিয়ে যান', blockButton__phoneCode: '{{identifier}}-এ এসএমএস কোড পাঠান', @@ -1626,6 +1627,10 @@ export const bnIN: LocalizationResource = { subtitle: 'চালিয়ে যেতে আপনার ইমেইলে পাঠানো কোড লিখুন', title: 'যাচাইকরণ প্রয়োজন', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'যাচাইকরণ চালিয়ে যাওয়া যাচ্ছে না। কোনো উপযুক্ত অথেনটিকেশন ফ্যাক্টর কনফিগার করা নেই', subtitle: 'একটি ত্রুটি ঘটেছে', diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index 9225a3c42c5..b47b2b132aa 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -1604,6 +1604,7 @@ export const caES: LocalizationResource = { actionText: 'No tens accés a aquest mètode? Prova una altra opció.', blockButton__backupCode: 'Utilitzar codi de seguretat', blockButton__emailCode: 'Utilitzar codi de correu electrònic', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Utilitzar contrasenya', blockButton__phoneCode: 'Utilitzar codi de telèfon', @@ -1627,6 +1628,10 @@ export const caES: LocalizationResource = { subtitle: "Comprova el codi de verificació a la teva bústia d'entrada.", title: 'Verificació per correu electrònic', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Ho sentim, no tens cap mètode de verificació disponible. Contacta amb suport.', subtitle: "No s'han trobat mètodes alternatius disponibles.", diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index cc16a2a3d70..0864650e6ea 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -1601,6 +1601,7 @@ export const csCZ: LocalizationResource = { actionText: 'Nemáte žádnou z těchto možností?', blockButton__backupCode: 'Použít záložní kód', blockButton__emailCode: 'Odeslat kód na e-mail {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Použít váš přístupový klíč', blockButton__password: 'Pokračovat s vaším heslem', blockButton__phoneCode: 'Odeslat SMS kód na {{identifier}}', @@ -1624,6 +1625,10 @@ export const csCZ: LocalizationResource = { subtitle: 'Zadejte kód odeslaný na váš e-mail pro pokračování', title: 'Vyžadováno ověření', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nelze pokračovat s ověřením. Není nakonfigurován žádný vhodný autentizační faktor', subtitle: 'Došlo k chybě', diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 362723d2869..02ea5b8fd8c 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -1595,6 +1595,7 @@ export const daDK: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1617,6 +1618,10 @@ export const daDK: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index 52ecf5ecb8b..022753affd1 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -1610,6 +1610,7 @@ export const deDE: LocalizationResource = { actionText: 'Verwenden Sie eine alternative Verifizierungsmethode', blockButton__backupCode: 'Mit Wiederherstellungscode verifizieren', blockButton__emailCode: 'Mit E-Mail-Code verifizieren', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Verwenden Sie Ihren Passkey', blockButton__password: 'Mit Passwort verifizieren', blockButton__phoneCode: 'Mit SMS-Code verifizieren', @@ -1633,6 +1634,10 @@ export const deDE: LocalizationResource = { subtitle: 'Überprüfen Sie Ihre E-Mail auf den Verifizierungscode.', title: 'E-Mail-Code Verifizierung', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Es sind keine Verifizierungsmethoden mehr verfügbar.', subtitle: 'Bitte kontaktieren Sie den Support, um Hilfe zu erhalten.', diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 5013b35ca5b..ad346e8da18 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -1600,6 +1600,7 @@ export const elGR: LocalizationResource = { actionText: 'Δεν έχετε κάποιο από αυτά;', blockButton__backupCode: 'Χρησιμοποιήστε εφεδρικό κωδικό', blockButton__emailCode: 'Αποστολή κωδικού στο {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Χρησιμοποιήστε το passkey σας', blockButton__password: 'Σύνδεση με τον κωδικό πρόσβασής σας', blockButton__phoneCode: 'Αποστολή κωδικού μέσω SMS στο {{identifier}}', @@ -1624,6 +1625,10 @@ export const elGR: LocalizationResource = { subtitle: 'για να συνεχίσετε', title: 'Ελέγξτε το email σας', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Δεν είναι δυνατή η συνέχεια. Δεν υπάρχει διαθέσιμος παράγοντας επαλήθευσης.', subtitle: 'Παρουσιάστηκε σφάλμα', diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index a038681c897..00b54228b40 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -1595,6 +1595,7 @@ export const enGB: LocalizationResource = { actionText: 'Don’t have any of these?', blockButton__backupCode: 'Use a backup code', blockButton__emailCode: 'Email code to {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Continue with your password', blockButton__phoneCode: 'Send SMS code to {{identifier}}', @@ -1618,6 +1619,10 @@ export const enGB: LocalizationResource = { subtitle: 'Enter the code sent to your email to continue', title: 'Verification required', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Cannot proceed with verification. No suitable authentication factor is configured', subtitle: 'An error occurred', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index b307ffe16d6..9393e2e8935 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -1641,6 +1641,7 @@ export const enUS: LocalizationResource = { actionText: 'Don’t have any of these?', blockButton__backupCode: 'Use a backup code', blockButton__emailCode: 'Email code to {{identifier}}', + blockButton__enterpriseSSO: 'Continue with {{name}}', blockButton__passkey: 'Use your passkey', blockButton__password: 'Continue with your password', blockButton__phoneCode: 'Send SMS code to {{identifier}}', @@ -1664,6 +1665,10 @@ export const enUS: LocalizationResource = { subtitle: 'Enter the code sent to your email to continue', title: 'Verification required', }, + enterpriseSSO: { + subtitle: 'Continue with your enterprise account to confirm it’s you', + title: 'Verification required', + }, noAvailableMethods: { message: 'Cannot proceed with verification. No suitable authentication factor is configured', subtitle: 'An error occurred', diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index ed06e9d92f1..19b2324cf8e 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -1600,6 +1600,7 @@ export const esCR: LocalizationResource = { actionText: '¿No posees ninguno de estos?', blockButton__backupCode: 'Utiliza un código de respaldo', blockButton__emailCode: 'Envía el código a {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Utiliza tu llave de acceso', blockButton__password: 'Continúa con tu contraseña', blockButton__phoneCode: 'Enviar un mensaje de texto a {{identifier}}', @@ -1622,6 +1623,10 @@ export const esCR: LocalizationResource = { subtitle: 'para continuar con {{applicationName}}', title: 'Revisa tu correo electrónico', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'No se puede proceder con el inicio de sesión. No hay ningún factor de autenticación disponible', subtitle: 'Ocurrió un error', diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index cff65d2b6a5..5469fee7da8 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -1605,6 +1605,7 @@ export const esES: LocalizationResource = { actionText: '¿No tienes acceso a este método? Prueba otra opción.', blockButton__backupCode: 'Usar código de respaldo', blockButton__emailCode: 'Usar código de correo electrónico', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Usar contraseña', blockButton__phoneCode: 'Usar código de teléfono', @@ -1628,6 +1629,10 @@ export const esES: LocalizationResource = { subtitle: 'Revisa tu bandeja de entrada para el código de verificación.', title: 'Verificación por correo electrónico', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Lo sentimos, no tienes ningún método de verificación disponible. Contacta con soporte.', subtitle: 'No se encontraron métodos alternativos disponibles.', diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index 2e6d7e4256d..8dd4cbf4831 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -1601,6 +1601,7 @@ export const esMX: LocalizationResource = { actionText: '¿No posees ninguno de estos?', blockButton__backupCode: 'Utiliza un código de respaldo', blockButton__emailCode: 'Envía el código a {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Utiliza tu llave de acceso', blockButton__password: 'Continúa con tu contraseña', blockButton__phoneCode: 'Enviar un mensaje de texto a {{identifier}}', @@ -1623,6 +1624,10 @@ export const esMX: LocalizationResource = { subtitle: 'para continuar con {{applicationName}}', title: 'Revisa tu correo electrónico', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'No se puede proceder con el inicio de sesión. No hay ningún factor de autenticación disponible', subtitle: 'Ocurrió un error', diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 395d82d04e4..b6fec692c29 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -1599,6 +1599,7 @@ export const esUY: LocalizationResource = { actionText: '¿No tenés ninguno de estos?', blockButton__backupCode: 'Usar un código de respaldo', blockButton__emailCode: 'Enviar código por correo a {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Continuar con tu contraseña', blockButton__phoneCode: 'Enviar código SMS a {{identifier}}', @@ -1622,6 +1623,10 @@ export const esUY: LocalizationResource = { subtitle: 'Ingresá el código enviado a tu correo para continuar', title: 'Verificación requerida', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'No se puede proceder con la verificación. No hay un factor de autenticación adecuado configurado', subtitle: 'Ocurrió un error', diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 38ce6f62bba..1c0e8323aa1 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -1603,6 +1603,7 @@ export const faIR: LocalizationResource = { actionText: 'هیچ کدام از اینها را ندارید؟', blockButton__backupCode: 'از کد پشتیبان استفاده کنید', blockButton__emailCode: 'کد را به {{identifier}} ایمیل کنید', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'از کلید عبور خود استفاده کنید', blockButton__password: 'با رمز عبور خود ادامه دهید', blockButton__phoneCode: 'ارسال کد پیامکی به {{identifier}}', @@ -1626,6 +1627,10 @@ export const faIR: LocalizationResource = { subtitle: 'برای ادامه، کد ارسال شده به ایمیل خود را وارد کنید', title: 'تایید هویت الزامی است', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'امکان ادامه تأیید وجود ندارد. هیچ عامل احراز هویت مناسبی پیکربندی نشده است.', subtitle: 'خطایی رخ داد', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 5e68744f92e..c551fc1c697 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -1605,6 +1605,7 @@ export const fiFI: LocalizationResource = { actionText: 'Eikö mikään näistä ole käytettävissä?', blockButton__backupCode: 'Käytä varakoodia', blockButton__emailCode: 'Lähetä koodi sähköpostitse {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Käytä pääsyavaintasi', blockButton__password: 'Jatka salasanallasi', blockButton__phoneCode: 'Lähetä SMS-koodi {{identifier}}', @@ -1628,6 +1629,10 @@ export const fiFI: LocalizationResource = { subtitle: 'Syötä sähköpostiisi lähetetty koodi jatkaaksesi', title: 'Vahvistus vaaditaan', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Vahvistusta ei voida suorittaa. Sopivaa todennusmenetelmää ei ole määritetty.', subtitle: 'Tapahtui virhe', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index b10aa5e3a60..c74118a2440 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -1611,6 +1611,7 @@ export const frFR: LocalizationResource = { actionText: 'Vous ne pouvez pas accéder à votre compte ?', blockButton__backupCode: 'Utiliser un code de récupération', blockButton__emailCode: 'Recevoir un code par e-mail', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Utiliser une clé de sécurité', blockButton__password: 'Utiliser le mot de passe', blockButton__phoneCode: 'Recevoir un code par téléphone', @@ -1634,6 +1635,10 @@ export const frFR: LocalizationResource = { subtitle: 'Un code a été envoyé à votre adresse e-mail.', title: 'Vérification par e-mail', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: "Aucune méthode de vérification n'est disponible.", subtitle: 'Impossible de procéder à la vérification.', diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 76d4e71749c..ba12e2cba61 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -1590,6 +1590,7 @@ export const heIL: LocalizationResource = { actionText: 'אין לך אף אחד מאלה?', blockButton__backupCode: 'השתמש בקוד גיבוי', blockButton__emailCode: 'קוד אימייל ל {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'המשך עם הסיסמה שלך', blockButton__phoneCode: 'שלח קוד SMS ל {{identifier}}', @@ -1612,6 +1613,10 @@ export const heIL: LocalizationResource = { subtitle: 'המשך ל {{applicationName}}', title: 'בדוק את האימייל שלך', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'לא ניתן להמשיך עם האימות. אין גורם אימות זמין', subtitle: 'קרתה תקלה', diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index a15280fb922..745b1179bb6 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -1603,6 +1603,7 @@ export const hiIN: LocalizationResource = { actionText: 'इनमें से कोई भी नहीं है?', blockButton__backupCode: 'बैकअप कोड का उपयोग करें', blockButton__emailCode: '{{identifier}} पर ईमेल कोड', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'अपनी पासकी का उपयोग करें', blockButton__password: 'अपने पासवर्ड के साथ जारी रखें', blockButton__phoneCode: '{{identifier}} पर SMS कोड भेजें', @@ -1626,6 +1627,10 @@ export const hiIN: LocalizationResource = { subtitle: 'जारी रखने के लिए अपने ईमेल पर भेजे गए कोड को दर्ज करें', title: 'सत्यापन आवश्यक है', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'सत्यापन जारी नहीं रख सकते। कोई उपयुक्त प्रमाणीकरण कारक कॉन्फ़िगर नहीं है', subtitle: 'एक त्रुटि हुई', diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index 898e1668daf..56371022c3c 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -1604,6 +1604,7 @@ export const hrHR: LocalizationResource = { actionText: 'Nemate ništa od ovoga?', blockButton__backupCode: 'Koristite rezervni kod', blockButton__emailCode: 'Pošalji kod e-poštom na {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Koristite svoj pristupni ključ', blockButton__password: 'Nastavite s vašom lozinkom', blockButton__phoneCode: 'Pošalji SMS kod na {{identifier}}', @@ -1627,6 +1628,10 @@ export const hrHR: LocalizationResource = { subtitle: 'Unesite kod poslan na vašu e-poštu za nastavak', title: 'Potrebna verifikacija', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Ne može se nastaviti s verifikacijom. Nema dostupnog faktora autentifikacije.', subtitle: 'Došlo je do pogreške', diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index ba75bd80414..8eae273e66c 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -1606,6 +1606,7 @@ export const huHU: LocalizationResource = { actionText: 'Nincs ezekből egyik sem?', blockButton__backupCode: 'Tartalék kód használata', blockButton__emailCode: 'Email kód küldése: {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Passkey használata', blockButton__password: 'Folytatás jelszóval', blockButton__phoneCode: 'SMS kód küldése: {{identifier}}', @@ -1629,6 +1630,10 @@ export const huHU: LocalizationResource = { subtitle: 'Írd be az e-mail címedre küldött kódot a folytatáshoz', title: 'Ellenőrzés szükséges', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nem lehet folytatni az ellenőrzést. Nincs konfigurált hitelesítési módszer.', subtitle: 'Hiba történt', diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index 7b35077a8b8..d264c682fdf 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -1598,6 +1598,7 @@ export const idID: LocalizationResource = { actionText: 'Tidak memiliki salah satu dari ini?', blockButton__backupCode: 'Gunakan kode cadangan', blockButton__emailCode: 'Kirim kode ke {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Lanjutkan dengan kata sandi Anda', blockButton__phoneCode: 'Kirim kode SMS ke {{identifier}}', @@ -1621,6 +1622,10 @@ export const idID: LocalizationResource = { subtitle: 'Masukkan kode yang dikirim ke email Anda untuk melanjutkan', title: 'Verifikasi diperlukan', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Tidak dapat melanjutkan verifikasi. Tidak ada faktor autentikasi yang sesuai dikonfigurasi', subtitle: 'Terjadi kesalahan', diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index f015eed4f00..307d48df2a9 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -1605,6 +1605,7 @@ export const isIS: LocalizationResource = { actionText: 'Ertu ekki með neitt af þessu?', blockButton__backupCode: 'Nota öryggiskóða', blockButton__emailCode: 'Senda kóða á {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Nota lykilinn þinn', blockButton__password: 'Halda áfram með lykilorði', blockButton__phoneCode: 'Senda SMS kóða á {{identifier}}', @@ -1628,6 +1629,10 @@ export const isIS: LocalizationResource = { subtitle: 'Sláðu inn kóðann sem sendur var á netfangið þitt til að halda áfram', title: 'Staðfesting nauðsynleg', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Ekki er hægt að halda áfram með staðfestingu. Engin viðeigandi auðkenningaraðferð er stillt.', subtitle: 'Villa kom upp', diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 4044db1665a..5703e6df935 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -1605,6 +1605,7 @@ export const itIT: LocalizationResource = { actionText: 'Usa un metodo di verifica alternativo', blockButton__backupCode: 'Verifica con il codice di backup', blockButton__emailCode: 'Verifica con il codice via email', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Verifica con la password', blockButton__phoneCode: 'Verifica con il codice SMS', @@ -1627,6 +1628,10 @@ export const itIT: LocalizationResource = { subtitle: 'Controlla la tua email per il codice di verifica.', title: 'Verifica con il codice email', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Non sono disponibili metodi di verifica.', subtitle: 'Contatta il supporto per assistenza.', diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index e0298371964..477fe5f8ff6 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -1604,6 +1604,7 @@ export const jaJP: LocalizationResource = { actionText: 'これらのいずれもお持ちでないですか?', blockButton__backupCode: 'バックアップコードを使用する', blockButton__emailCode: '{{identifier}} にメールコードを送信', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'パスキーを使用する', blockButton__password: 'パスワードで続行', blockButton__phoneCode: '{{identifier}} にSMSコードを送信', @@ -1627,6 +1628,10 @@ export const jaJP: LocalizationResource = { subtitle: '続行するには、メールに送信されたコードを入力してください', title: '確認が必要です', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: '確認を続行できません。利用可能な認証要素が設定されていません。', subtitle: 'エラーが発生しました', diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 8a51cb18eca..7b030de199e 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -1588,6 +1588,7 @@ export const kkKZ: LocalizationResource = { actionText: 'Бұл опциялар жоқ па?', blockButton__backupCode: 'Сақтық кодын қолдану', blockButton__emailCode: '{{identifier}} электрондық поштасына код жіберу', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Passkey қолдану', blockButton__password: 'Құпия сөзбен жалғастыру', blockButton__phoneCode: '{{identifier}} нөміріне SMS жіберу', @@ -1610,6 +1611,10 @@ export const kkKZ: LocalizationResource = { subtitle: 'Жалғастыру үшін электрондық поштаңызға жіберілген кодты енгізіңіз', title: 'Растау қажет', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Растау мүмкін емес. Ешбір аутентификация әдісі қосылмаған.', subtitle: 'Қате орын алды', diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index abec99a1dff..a8208c41bec 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -1594,6 +1594,7 @@ export const koKR: LocalizationResource = { actionText: '이 방법이 없나요?', blockButton__backupCode: '백업 코드 사용', blockButton__emailCode: '{{identifier}}로 이메일 코드 보내기', + blockButton__enterpriseSSO: undefined, blockButton__passkey: '패스키 사용하기', blockButton__password: '비밀번호로 계속', blockButton__phoneCode: '{{identifier}}로 SMS 코드 보내기', @@ -1616,6 +1617,10 @@ export const koKR: LocalizationResource = { subtitle: '이메일로 전송된 코드를 입력해 주세요', title: '인증이 필요해요', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: '인증을 진행할 수 없어요. 설정된 인증 수단이 없어요.', subtitle: '오류가 발생했어요', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index 64c73cef2a1..19ad67bfc11 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -1598,6 +1598,7 @@ export const mnMN: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1620,6 +1621,10 @@ export const mnMN: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 99c926f3155..c2a464fcc27 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -1607,6 +1607,7 @@ export const msMY: LocalizationResource = { actionText: 'Tidak mempunyai mana-mana ini?', blockButton__backupCode: 'Gunakan kod sandaran', blockButton__emailCode: 'E-mel kod ke {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Gunakan kunci pas anda', blockButton__password: 'Teruskan dengan kata laluan anda', blockButton__phoneCode: 'Hantar kod SMS ke {{identifier}}', @@ -1630,6 +1631,10 @@ export const msMY: LocalizationResource = { subtitle: 'Masukkan kod yang dihantar ke e-mel anda untuk meneruskan', title: 'Pengesahan diperlukan', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Tidak dapat meneruskan pengesahan. Tiada faktor pengesahan yang sesuai dikonfigurasi', subtitle: 'Ralat berlaku', diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 0ebbf9b3315..469ba84c313 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -1606,6 +1606,7 @@ export const nbNO: LocalizationResource = { actionText: 'Har du ingen av disse?', blockButton__backupCode: 'Bruk en sikkerhetskode', blockButton__emailCode: 'Send e-postkode til {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Bruk passnøkkelen din', blockButton__password: 'Fortsett med passordet ditt', blockButton__phoneCode: 'Send SMS-kode til {{identifier}}', @@ -1629,6 +1630,10 @@ export const nbNO: LocalizationResource = { subtitle: 'Skriv inn koden sendt til e-posten din for å fortsette', title: 'Verifisering påkrevd', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Kan ikke fortsette med verifiseringen. Ingen passende autentiseringsfaktor er konfigurert', subtitle: 'En feil oppstod', diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index 8ceaa13c70a..edc4b17c418 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -1598,6 +1598,7 @@ export const nlBE: LocalizationResource = { actionText: 'Heb je geen van deze?', blockButton__backupCode: 'Backupcode gebruiken', blockButton__emailCode: 'Email code naar {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Doorgaan met je wachtwoord', blockButton__phoneCode: 'Verzend SMS code naar {{identifier}}', @@ -1621,6 +1622,10 @@ export const nlBE: LocalizationResource = { subtitle: 'om door te gaan naar {{applicationName}}', title: 'Controleer je email', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Kan niet verder gaan met verificatie. Er is geen beschikbare verificatiefactor.', subtitle: 'Er is een fout opgetreden', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 762a4f68899..eb344ec9254 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -1598,6 +1598,7 @@ export const nlNL: LocalizationResource = { actionText: 'Heb je geen van deze?', blockButton__backupCode: 'Backupcode gebruiken', blockButton__emailCode: 'Email code naar {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Doorgaan met je wachtwoord', blockButton__phoneCode: 'Verzend SMS code naar {{identifier}}', @@ -1621,6 +1622,10 @@ export const nlNL: LocalizationResource = { subtitle: 'om door te gaan naar {{applicationName}}', title: 'Controleer je email', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Kan niet verder gaan met verificatie. Er is geen beschikbare verificatiefactor.', subtitle: 'Er is een fout opgetreden', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 19eed5d20d3..330ce310059 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -1596,6 +1596,7 @@ export const plPL: LocalizationResource = { actionText: 'Nie używasz żadnej z tych metod?', blockButton__backupCode: 'Użyj kodu zapasowego', blockButton__emailCode: 'Wyślij kod e-mailem do {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Zaloguj się za pomocą hasła', blockButton__phoneCode: 'Wyślij kod SMS-em do {{identifier}}', @@ -1619,6 +1620,10 @@ export const plPL: LocalizationResource = { subtitle: 'Wprowadź kod wysłany na Twój adres e-mail, aby kontynuować', title: 'Wymagana weryfikacja', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nie można kontynuować weryfikacji. Brak dostępnych czynników uwierzytelniania.', subtitle: 'Wystąpił błąd', diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 0b265682603..d279e93e0e3 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -1606,6 +1606,7 @@ export const ptBR: LocalizationResource = { actionText: 'Não tem nenhum dos métodos? Tente outra forma.', blockButton__backupCode: 'Usar código de backup', blockButton__emailCode: 'Enviar código para {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Usar sua chave de acesso', blockButton__password: 'Usar senha', blockButton__phoneCode: 'Enviar código de telefone', @@ -1628,6 +1629,10 @@ export const ptBR: LocalizationResource = { subtitle: 'Verifique seu e-mail e insira o código para continuar.', title: 'Verifique seu e-mail', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nenhum método de verificação disponível. Entre em contato com o suporte.', subtitle: 'Não há métodos de verificação disponíveis no momento.', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 72e81ceabb0..8b4cb69aa95 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -1606,6 +1606,7 @@ export const ptPT: LocalizationResource = { actionText: 'Não tem nenhum destes métodos?', blockButton__backupCode: 'Utilizar um código de recuperação', blockButton__emailCode: 'Código por e-mail para {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Utilizar a sua chave de acesso', blockButton__password: 'Continuar com a sua palavra-passe', blockButton__phoneCode: 'Enviar código SMS para {{identifier}}', @@ -1629,6 +1630,10 @@ export const ptPT: LocalizationResource = { subtitle: 'Introduza o código enviado para o seu e-mail para continuar', title: 'Verificação necessária', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Não é possível prosseguir com a verificação. Não existe um fator de autenticação adequado configurado.', subtitle: 'Ocorreu um erro', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index d8515f1b2ae..b81b3a28087 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -1606,6 +1606,7 @@ export const roRO: LocalizationResource = { actionText: 'Nu ai niciuna dintre acestea?', blockButton__backupCode: 'Folosește un cod de rezervă', blockButton__emailCode: 'Trimite cod pe email către {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Folosește cheia de acces', blockButton__password: 'Continuă cu parola', blockButton__phoneCode: 'Trimite cod prin SMS la {{identifier}}', @@ -1629,6 +1630,10 @@ export const roRO: LocalizationResource = { subtitle: 'Introdu codul trimis pe email pentru a continua', title: 'Verificare necesară', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nu se poate continua. Nu există un factor de autentificare configurat.', subtitle: 'A apărut o eroare', diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 3af75fb03eb..6a30a0b47ef 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -1603,6 +1603,7 @@ export const ruRU: LocalizationResource = { actionText: 'У вас нет ничего из этого?', blockButton__backupCode: 'Использовать резервный код', blockButton__emailCode: 'Отправить код на {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Продолжить с вашим паролем', blockButton__phoneCode: 'Отправить SMS код на {{identifier}}', @@ -1626,6 +1627,10 @@ export const ruRU: LocalizationResource = { subtitle: 'для продолжения {{applicationName}}', title: 'Проверьте вашу почту', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Невозможно продолжить верификацию. Нет доступного фактора аутентификации.', subtitle: 'Произошла ошибка', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index 1dcc2f679d6..c7c590f37e5 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -1596,6 +1596,7 @@ export const skSK: LocalizationResource = { actionText: 'Nemáte prístup k žiadnej z týchto metód?', blockButton__backupCode: 'Použiť záložný kód', blockButton__emailCode: 'Odoslať overovací kód na email {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Použiť Passkey', blockButton__password: 'Pokračovať pomocou hesla', blockButton__phoneCode: 'Poslať SMS kód na telefónne číslo {{identifier}}', @@ -1619,6 +1620,10 @@ export const skSK: LocalizationResource = { subtitle: 'Pre pokračovanie vložte kód odoslaný na váš email', title: 'Vyžaduje sa overenie', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Nemožno pokračovať v overení. Nie je nastavená žiadna dostupná autentifikačná metóda.', subtitle: 'Došlo k chybe', diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index 77c45b2f883..1db25829328 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -1595,6 +1595,7 @@ export const srRS: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1617,6 +1618,10 @@ export const srRS: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index cec06c14be5..959b70972e0 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -1596,6 +1596,7 @@ export const svSE: LocalizationResource = { actionText: 'Har du inget av dessa?', blockButton__backupCode: 'Använd en reservkod', blockButton__emailCode: 'Skicka kod via e-post till {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Fortsätt med ditt lösenord', blockButton__phoneCode: 'Skicka SMS-kod till {{identifier}}', @@ -1619,6 +1620,10 @@ export const svSE: LocalizationResource = { subtitle: 'för att fortsätta till {{applicationName}}', title: 'Kontrollera din e-post', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Kan inte fortsätta med verifieringen. Det finns ingen tillgänglig autentiseringsfaktor.', subtitle: 'Ett fel inträffade', diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index 5dbb4287d9c..f20c1a6295e 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -1609,6 +1609,7 @@ export const taIN: LocalizationResource = { actionText: 'இவற்றில் எதுவும் இல்லையா?', blockButton__backupCode: 'காப்புக் குறியீட்டைப் பயன்படுத்தவும்', blockButton__emailCode: '{{identifier}} க்கு மின்னஞ்சல் குறியீடு', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'உங்கள் பாஸ்கீயைப் பயன்படுத்தவும்', blockButton__password: 'உங்கள் கடவுச்சொல்லுடன் தொடரவும்', blockButton__phoneCode: '{{identifier}} க்கு SMS குறியீடு அனுப்பவும்', @@ -1632,6 +1633,10 @@ export const taIN: LocalizationResource = { subtitle: 'தொடர உங்கள் மின்னஞ்சலுக்கு அனுப்பப்பட்ட குறியீட்டை உள்ளிடவும்', title: 'சரிபார்ப்பு தேவை', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'சரிபார்ப்பைத் தொடர முடியாது. பொருத்தமான அங்கீகார காரணி எதுவும் கட்டமைக்கப்படவில்லை', subtitle: 'பிழை ஏற்பட்டது', diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index 341a072a8c1..120404712b7 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -1606,6 +1606,7 @@ export const teIN: LocalizationResource = { actionText: 'వీటిలో ఏవీ లేవా?', blockButton__backupCode: 'బ్యాకప్ కోడ్‌ను ఉపయోగించండి', blockButton__emailCode: '{{identifier}}కి ఇమెయిల్ కోడ్', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'మీ పాస్‌కీని ఉపయోగించండి', blockButton__password: 'మీ పాస్‌వర్డ్‌తో కొనసాగించండి', blockButton__phoneCode: '{{identifier}}కి SMS కోడ్‌ను పంపండి', @@ -1629,6 +1630,10 @@ export const teIN: LocalizationResource = { subtitle: 'కొనసాగించడానికి మీ ఇమెయిల్‌కి పంపిన కోడ్‌ను నమోదు చేయండి', title: 'ధృవీకరణ అవసరం', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'ధృవీకరణతో కొనసాగలేము. సరైన ప్రమాణీకరణ కారకం కాన్ఫిగర్ చేయబడలేదు', subtitle: 'లోపం సంభవించింది', diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 51dcfa5b0e9..4a7410092e3 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -1596,6 +1596,7 @@ export const thTH: LocalizationResource = { actionText: 'ไม่สามารถเข้าถึงวิธีเหล่านี้?', blockButton__backupCode: 'ใช้รหัสสำรอง', blockButton__emailCode: 'ส่งรหัสทางอีเมลไปยัง {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'ใช้พาสคีย์ของคุณ', blockButton__password: 'ดำเนินการต่อด้วยรหัสผ่านของคุณ', blockButton__phoneCode: 'ส่งรหัส SMS ไปยัง {{identifier}}', @@ -1619,6 +1620,10 @@ export const thTH: LocalizationResource = { subtitle: 'ใส่รหัสที่ส่งไปยังอีเมลของคุณเพื่อดำเนินการต่อ', title: 'ต้องการการยืนยัน', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'ไม่สามารถดำเนินการยืนยันได้ ไม่มีปัจจัยการยืนยันตัวตนที่เหมาะสมได้รับการกำหนดค่า', subtitle: 'เกิดข้อผิดพลาด', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index 50d01c57240..a17c45e7222 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -1596,6 +1596,7 @@ export const trTR: LocalizationResource = { actionText: 'Alternatif doğrulama yöntemlerini kullanmak ister misiniz?', blockButton__backupCode: 'Yedek kodu kullan', blockButton__emailCode: 'E-posta kodu gönder', + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: 'Şifreyi gir', blockButton__phoneCode: 'Telefon kodu gönder', @@ -1618,6 +1619,10 @@ export const trTR: LocalizationResource = { subtitle: 'E-posta adresinize gönderilen doğrulama kodunu girin.', title: 'E-posta Kodunu Girin', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Alternatif doğrulama yöntemleri mevcut değil.', subtitle: 'Lütfen farklı bir doğrulama yöntemi deneyin.', diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index 9125e2b878c..076a555c682 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -1595,6 +1595,7 @@ export const ukUA: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1617,6 +1618,10 @@ export const ukUA: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index 1b5a87abe44..559736d257e 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -1603,6 +1603,7 @@ export const viVN: LocalizationResource = { actionText: 'Không có bất kỳ phương thức nào?', blockButton__backupCode: 'Sử dụng mã dự phòng', blockButton__emailCode: 'Email mã đến {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: 'Đăng nhập với mã passkey', blockButton__password: 'Tiếp tục với mật khẩu của bạn', blockButton__phoneCode: 'Gửi mã SMS đến {{identifier}}', @@ -1626,6 +1627,10 @@ export const viVN: LocalizationResource = { subtitle: 'Nhập mã đã gửi đến email của bạn để tiếp tục', title: 'Xác minh yêu cầu', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: 'Không thể tiếp tục với xác minh. Không có yếu tố xác thực phù hợp được cấu hình', subtitle: 'Đã xảy ra lỗi', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 3e65172457b..eedcb674afc 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -1586,6 +1586,7 @@ export const zhCN: LocalizationResource = { actionText: undefined, blockButton__backupCode: undefined, blockButton__emailCode: undefined, + blockButton__enterpriseSSO: undefined, blockButton__passkey: undefined, blockButton__password: undefined, blockButton__phoneCode: undefined, @@ -1608,6 +1609,10 @@ export const zhCN: LocalizationResource = { subtitle: undefined, title: undefined, }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: undefined, subtitle: undefined, diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index 3d905a96077..0f6498e9df0 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -1589,6 +1589,7 @@ export const zhTW: LocalizationResource = { actionText: '沒有以上任何一種方式嗎?', blockButton__backupCode: '使用備用碼', blockButton__emailCode: '傳送驗證碼至 {{identifier}}', + blockButton__enterpriseSSO: undefined, blockButton__passkey: '使用您的金鑰', blockButton__password: '使用您的密碼登入', blockButton__phoneCode: '傳送簡訊代碼至 {{identifier}}', @@ -1611,6 +1612,10 @@ export const zhTW: LocalizationResource = { subtitle: '請輸入已寄至您電子郵件的驗證碼以繼續', title: '需要驗證您的身分', }, + enterpriseSSO: { + subtitle: undefined, + title: undefined, + }, noAvailableMethods: { message: '無法繼續驗證。沒有可用的驗證方式。', subtitle: '發生錯誤', diff --git a/packages/shared/src/internal/clerk-js/popupCallback.ts b/packages/shared/src/internal/clerk-js/popupCallback.ts new file mode 100644 index 00000000000..295c162914a --- /dev/null +++ b/packages/shared/src/internal/clerk-js/popupCallback.ts @@ -0,0 +1,59 @@ +import { buildAccountsBaseUrl } from '../../buildAccountsBaseUrl'; +import { ClerkRuntimeError } from '../../errors/clerkRuntimeError'; + +export const POPUP_CLOSED_ERROR_CODE = 'popup_closed'; + +export function buildPopupCallbackUrls( + frontendApi: string, + buildUrlWithAuth: (url: string) => string, + returnUrl: string, +): { accountsOrigin: string; redirectUrl: string; redirectUrlComplete: string } { + const accountsOrigin = buildAccountsBaseUrl(frontendApi); + return { + accountsOrigin, + redirectUrl: buildUrlWithAuth(`${accountsOrigin}/popup-callback?return_url=${encodeURIComponent(returnUrl)}`), + redirectUrlComplete: buildUrlWithAuth(`${accountsOrigin}/popup-callback`), + }; +} + +export function waitForPopupCallback(popup: Window, url: string, accountsOrigin: string): Promise { + return new Promise((resolve, reject) => { + let settled = false; + const settle = (fn: () => void) => { + if (settled) { + return; + } + settled = true; + window.removeEventListener('message', onMessage); + clearInterval(interval); + fn(); + }; + const onMessage = (event: MessageEvent) => { + if (event.origin !== accountsOrigin || (event.source && event.source !== popup)) { + return; + } + if (event.data?.session || event.data?.return_url) { + settle(resolve); + } + }; + const interval = setInterval(() => { + if (popup.closed) { + clearInterval(interval); + setTimeout( + () => + settle(() => + reject( + new ClerkRuntimeError('The popup was closed before the flow completed', { + code: POPUP_CLOSED_ERROR_CODE, + }), + ), + ), + 1000, + ); + } + }, 500); + + window.addEventListener('message', onMessage); + popup.location.href = url; + }); +} diff --git a/packages/shared/src/react/hooks/useReverification.ts b/packages/shared/src/react/hooks/useReverification.ts index 3ebb8dba3c4..5a8f2856d0b 100644 --- a/packages/shared/src/react/hooks/useReverification.ts +++ b/packages/shared/src/react/hooks/useReverification.ts @@ -4,7 +4,7 @@ import { validateReverificationConfig } from '../../authorization'; import { isReverificationHint, reverificationError } from '../../authorization-errors'; import { ClerkRuntimeError, isClerkAPIResponseError } from '../../error'; import { eventMethodCalled } from '../../telemetry'; -import type { Clerk, SessionVerificationLevel } from '../../types'; +import type { Clerk, OAuthFlow, SessionVerificationLevel } from '../../types'; import { createDeferredPromise } from '../../utils/createDeferredPromise'; import { useClerk } from './useClerk'; import { useSafeLayoutEffect } from './useSafeLayoutEffect'; @@ -58,6 +58,12 @@ export type NeedsReverificationParameters = { * @interface */ export type UseReverificationOptions = { + /** + * Control whether enterprise SSO verification in the default UI uses a popup or a full-page redirect. `'auto'` follows the same heuristic as the sign-in component. + * + * @default `'popup'` + */ + oauthFlow?: OAuthFlow; /** * Handler for the reverification process. Opts out of using the default UI. Use this to build a custom UI. * @@ -140,6 +146,7 @@ function createReverificationHandler(params: CreateReverificationHandlerParams) */ params.openUIComponent?.({ level: level, + oauthFlow: params.oauthFlow, afterVerification: complete, afterVerificationCancelled: cancel, }); diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 069bc460a14..b5d15bb9c13 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1876,6 +1876,11 @@ export type RoutingOptions = | { path: string | undefined; routing?: Extract } | { path?: never; routing?: Extract }; +/** + * Whether an OAuth or enterprise SSO round trip opens in a popup or navigates the page. `'auto'` opens a popup only where the current origin supports it. + */ +export type OAuthFlow = 'auto' | 'redirect' | 'popup'; + /** @generateWithEmptyComment */ export type SignInProps = RoutingOptions & { /** @@ -1931,7 +1936,7 @@ export type SignInProps = RoutingOptions & { /** * Control whether OAuth flows use redirects or popups. */ - oauthFlow?: 'auto' | 'redirect' | 'popup'; + oauthFlow?: OAuthFlow; /** * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ @@ -1979,6 +1984,13 @@ export type __internal_UserVerificationProps = RoutingOptions & { */ afterVerificationCancelled?: () => void; + /** + * Control whether enterprise SSO verification uses a popup or a full-page redirect. `'auto'` follows the same heuristic as the sign-in component. + * + * @default `'popup'` + */ + oauthFlow?: OAuthFlow; + /** * Defines the steps of the verification flow. * When `multiFactor` is used, the user will be prompt for a first factor flow followed by a second factor flow. @@ -2114,7 +2126,7 @@ export type SignUpProps = RoutingOptions & { /** * Control whether OAuth flows use redirects or popups. */ - oauthFlow?: 'auto' | 'redirect' | 'popup'; + oauthFlow?: OAuthFlow; /** * Optional for `oauth_` or `enterprise_sso` strategies. The value to pass to the [OIDC prompt parameter](https://openid.net/specs/openid-connect-core-1_0.html#:~:text=prompt,reauthentication%20and%20consent.) in the generated OAuth redirect URL. */ diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 446d328a8e5..afe00b93fe9 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -654,6 +654,10 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; actionLink: LocalizationValue; }; + enterpriseSSO: { + title: LocalizationValue; + subtitle: LocalizationValue; + }; emailCode: { title: LocalizationValue; subtitle: LocalizationValue; @@ -702,6 +706,7 @@ export type __internal_LocalizationResource = { blockButton__totp: LocalizationValue; blockButton__passkey: LocalizationValue; blockButton__backupCode: LocalizationValue; + blockButton__enterpriseSSO: LocalizationValue<'name'>; getHelp: { title: LocalizationValue; content: LocalizationValue; diff --git a/packages/shared/src/types/session.ts b/packages/shared/src/types/session.ts index fdab3d21fa8..af359c6486d 100644 --- a/packages/shared/src/types/session.ts +++ b/packages/shared/src/types/session.ts @@ -540,7 +540,7 @@ export type SessionVerifyPrepareFirstFactorParams = /** * @experimental */ - | Omit + | (Omit & { actionCompleteRedirectUrl?: string }) /** * @experimental */ diff --git a/packages/ui/bundlewatch.config.json b/packages/ui/bundlewatch.config.json index 3bfffc77315..5996d76567e 100644 --- a/packages/ui/bundlewatch.config.json +++ b/packages/ui/bundlewatch.config.json @@ -5,7 +5,7 @@ { "path": "./dist/ui.shared.browser.js", "maxSize": "44KB" }, { "path": "./dist/framework*.js", "maxSize": "44KB" }, { "path": "./dist/vendors*.js", "maxSize": "73KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "139KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "140KB" }, { "path": "./dist/signin*.js", "maxSize": "19KB" }, { "path": "./dist/signup*.js", "maxSize": "13KB" }, { "path": "./dist/userprofile*.js", "maxSize": "16KB" }, diff --git a/packages/ui/src/common/ChooseEnterpriseConnectionCard.tsx b/packages/ui/src/common/ChooseEnterpriseConnectionCard.tsx index 97f10a5247f..32ef02db37f 100644 --- a/packages/ui/src/common/ChooseEnterpriseConnectionCard.tsx +++ b/packages/ui/src/common/ChooseEnterpriseConnectionCard.tsx @@ -15,6 +15,7 @@ type ChooseEnterpriseConnectionCardProps = { subtitle: LocalizationKey; onClick: (id: string) => Promise; enterpriseConnections: Array<{ id: string; name: string; logoPublicUrl?: string | null; provider?: string }>; + showLogo?: boolean; }; /** @@ -25,6 +26,7 @@ export const ChooseEnterpriseConnectionCard = ({ subtitle, onClick, enterpriseConnections, + showLogo = true, children, }: PropsWithChildren) => { const card = useCardState(); @@ -32,7 +34,7 @@ export const ChooseEnterpriseConnectionCard = ({ return ( - + diff --git a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx index a77193ea38f..55b55f3983f 100644 --- a/packages/ui/src/components/UserVerification/AlternativeMethods.tsx +++ b/packages/ui/src/components/UserVerification/AlternativeMethods.tsx @@ -118,6 +118,10 @@ export function getButtonLabel(factor: SessionVerificationFirstFactor): Localiza return localizationKeys('reverification.alternativeMethods.blockButton__password'); case 'passkey': return localizationKeys('reverification.alternativeMethods.blockButton__passkey'); + case 'enterprise_sso': + return localizationKeys('reverification.alternativeMethods.blockButton__enterpriseSSO', { + name: factor.enterpriseConnectionName || '', + }); default: throw new Error(`Invalid sign in strategy: "${(factor as any).strategy}"`); } diff --git a/packages/ui/src/components/UserVerification/UVFactorOneEnterpriseSSOCard.tsx b/packages/ui/src/components/UserVerification/UVFactorOneEnterpriseSSOCard.tsx new file mode 100644 index 00000000000..372f2a9ea18 --- /dev/null +++ b/packages/ui/src/components/UserVerification/UVFactorOneEnterpriseSSOCard.tsx @@ -0,0 +1,142 @@ +import { ClerkRuntimeError, isClerkAPIResponseError, isClerkRuntimeError } from '@clerk/shared/error'; +import { + buildPopupCallbackUrls, + POPUP_CLOSED_ERROR_CODE, + waitForPopupCallback, +} from '@clerk/shared/internal/clerk-js/popupCallback'; +import { useClerk, useSession } from '@clerk/shared/react'; +import type { EnterpriseSSOFactor } from '@clerk/shared/types'; +import React from 'react'; + +import { ChooseEnterpriseConnectionCard } from '@/ui/common/ChooseEnterpriseConnectionCard'; +import { Card } from '@/ui/elements/Card'; +import { useCardState } from '@/ui/elements/contexts'; +import { handleError } from '@/ui/utils/errorHandler'; +import { originPrefersPopup } from '@/ui/utils/originPrefersPopup'; + +import { useUserVerification } from '../../contexts'; +import { Flow, localizationKeys } from '../../customizables'; +import { useAfterVerification } from './use-after-verification'; +import { useUserVerificationSession } from './useUserVerificationSession'; + +type EnterpriseConnectionFactor = EnterpriseSSOFactor & { + enterpriseConnectionId: string; + enterpriseConnectionName: string; +}; + +type UVFactorOneEnterpriseSSOCardProps = { + factors: EnterpriseConnectionFactor[]; + onShowAlternativeMethodsClicked: React.MouseEventHandler | undefined; +}; + +export const UVFactorOneEnterpriseSSOCard = (props: UVFactorOneEnterpriseSSOCardProps) => { + const { factors, onShowAlternativeMethodsClicked } = props; + const clerk = useClerk(); + const { session } = useSession(); + const { data } = useUserVerificationSession(); + const { oauthFlow } = useUserVerification(); + const { handleVerificationResponse } = useAfterVerification(); + const card = useCardState(); + + const emailAddressIdFor = (factor: EnterpriseConnectionFactor) => { + const user = session?.user; + const account = user?.enterpriseAccounts.find( + account => + account.enterpriseConnectionId === factor.enterpriseConnectionId || + account.enterpriseConnection?.enterpriseConnectionId === factor.enterpriseConnectionId || + (account.provider === factor.enterpriseConnectionProvider && + account.enterpriseConnection?.name === factor.enterpriseConnectionName), + ); + const domain = account?.enterpriseConnection?.domain?.toLowerCase(); + return ( + user?.emailAddresses.find(email => email.emailAddress === account?.emailAddress)?.id ?? + user?.emailAddresses.find(email => !!domain && email.emailAddress.toLowerCase().endsWith(`@${domain}`))?.id + ); + }; + + const verifyWithEnterpriseSSO = async (enterpriseConnectionId: string, popup: Window | null): Promise => { + const factor = factors.find(f => f.enterpriseConnectionId === enterpriseConnectionId); + if (!session || !data || !factor) { + throw new ClerkRuntimeError('There is no active session to verify', { code: 'reverification_session_missing' }); + } + + const currentUrl = window.location.href; + const popupUrls = popup + ? buildPopupCallbackUrls(clerk.frontendApi, url => clerk.buildUrlWithAuth(url), currentUrl) + : null; + + const response = await session.prepareFirstFactorVerification({ + strategy: 'enterprise_sso', + enterpriseConnectionId, + emailAddressId: emailAddressIdFor(factor), + redirectUrl: popupUrls?.redirectUrl ?? currentUrl, + actionCompleteRedirectUrl: popupUrls?.redirectUrlComplete ?? currentUrl, + }); + + const url = response.firstFactorVerification.externalVerificationRedirectURL; + if (!url) { + throw new ClerkRuntimeError('Missing external verification redirect URL', { + code: 'external_verification_redirect_url_missing', + }); + } + + if (!popup || !popupUrls) { + await clerk.navigate(url.href); + return; + } + + await waitForPopupCallback(popup, url.href, popupUrls.accountsOrigin); + await session.reload(); + if (session.factorVerificationAge?.[0] !== 0) { + throw new ClerkRuntimeError('Verification with your identity provider did not complete', { + code: 'reverification_enterprise_sso_failed', + }); + } + + await handleVerificationResponse({ + ...data, + session, + status: data.level === 'multi_factor' ? 'needs_second_factor' : 'complete', + }); + }; + + const handleSelectEnterpriseConnection = (enterpriseConnectionId: string) => { + const usePopup = oauthFlow === 'redirect' ? false : oauthFlow === 'auto' ? originPrefersPopup() : true; + const popup = usePopup ? window.open('about:blank', '', 'width=600,height=800') : null; + return verifyWithEnterpriseSSO(enterpriseConnectionId, popup).catch(err => { + popup?.close(); + if (isClerkAPIResponseError(err)) { + card.setError(err.errors[0]); + } else if (!isClerkRuntimeError(err) || err.code !== POPUP_CLOSED_ERROR_CODE) { + handleError(err, [], card.setError); + } + throw err; + }); + }; + + return ( + + ({ + id: factor.enterpriseConnectionId, + name: factor.enterpriseConnectionName, + logoPublicUrl: factor.enterpriseConnectionLogoPublicUrl, + provider: factor.enterpriseConnectionProvider, + }))} + showLogo={false} + > + {onShowAlternativeMethodsClicked && ( + + + + )} + + + ); +}; diff --git a/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx b/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx index 73d73e5fbf5..babb32b2fc5 100644 --- a/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx +++ b/packages/ui/src/components/UserVerification/UserVerificationFactorOne.tsx @@ -1,3 +1,4 @@ +import { getEnterpriseConnectionFactors } from '@clerk/shared/internal/clerk-js/enterpriseSSOFactors'; import type { SessionVerificationFirstFactor, SignInFactor } from '@clerk/shared/types'; import React, { useEffect, useMemo } from 'react'; @@ -16,6 +17,7 @@ import { UserVerificationFactorOnePasswordCard } from './UserVerificationFactorO import { useUserVerificationSession, withUserVerificationSessionGuard } from './useUserVerificationSession'; import { sortByPrimaryFactor } from './utils'; import { UVFactorOneEmailCodeCard } from './UVFactorOneEmailCodeCard'; +import { UVFactorOneEnterpriseSSOCard } from './UVFactorOneEnterpriseSSOCard'; import { UVFactorOnePasskeysCard } from './UVFactorOnePasskeysCard'; import { UVFactorOnePhoneCodeCard } from './UVFactorOnePhoneCodeCard'; @@ -30,6 +32,9 @@ const factorKey = (factor: SignInFactor | null | undefined) => { if ('phoneNumberId' in factor) { key += factor.phoneNumberId; } + if ('enterpriseConnectionId' in factor) { + key += factor.enterpriseConnectionId; + } return key; }; @@ -38,8 +43,11 @@ const SUPPORTED_STRATEGIES: SessionVerificationFirstFactor['strategy'][] = [ 'email_code', 'phone_code', 'passkey', + 'enterprise_sso', ] as const; +const factorHasCard = (factor: SignInFactor) => factorHasLocalStrategy(factor) || factor.strategy === 'enterprise_sso'; + export function UserVerificationFactorOneInternal(): JSX.Element | null { const { data } = useUserVerificationSession(); const card = useCardState(); @@ -59,7 +67,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { const { preferredSignInStrategy } = useEnvironment().displayConfig; const webAuthnSupported = useIsWebAuthnSupported(); - const [{ currentFactor }, setFactor] = React.useState<{ + const [{ currentFactor, prevCurrentFactor }, setFactor] = React.useState<{ currentFactor: SignInFactor | undefined | null; prevCurrentFactor: SignInFactor | undefined | null; }>(() => ({ @@ -73,7 +81,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { }); const [showAllStrategies, setShowAllStrategies] = React.useState( - () => !currentFactor || !factorHasLocalStrategy(currentFactor), + () => !currentFactor || !factorHasCard(currentFactor), ); const toggleAllStrategies = hasAlternativeStrategies @@ -112,7 +120,7 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { } if (showAllStrategies) { - const canGoBack = factorHasLocalStrategy(currentFactor); + const canGoBack = factorHasCard(currentFactor); const toggle = toggleAllStrategies; const backHandler = () => { @@ -157,6 +165,17 @@ export function UserVerificationFactorOneInternal(): JSX.Element | null { ); case 'passkey': return ; + case 'enterprise_sso': + return ( + factorKey(f) === factorKey(currentFactor)) + : availableFactors, + )} + onShowAlternativeMethodsClicked={toggleAllStrategies} + /> + ); default: return ; } diff --git a/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx b/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx index a1ba3316620..87cce7d42be 100644 --- a/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx +++ b/packages/ui/src/components/UserVerification/__tests__/UVFactorOne.test.tsx @@ -1,5 +1,7 @@ +import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; +import { ClerkAPIResponseError } from '@clerk/shared/error'; import { waitFor } from '@testing-library/react'; -import { beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; import { render, screen } from '@/test/utils'; @@ -252,6 +254,302 @@ describe('UserVerificationFactorOne', () => { }); }); + describe('Enterprise SSO', () => { + const oktaFactor = { strategy: 'enterprise_sso', enterpriseConnectionId: 'ec_1', enterpriseConnectionName: 'Okta' }; + const entraFactor = { + strategy: 'enterprise_sso', + enterpriseConnectionId: 'ec_2', + enterpriseConnectionName: 'Entra', + }; + const redirectUrl = new URL('https://clerk.example.com/v1/verify?token=abc'); + + it('renders a button for the connection when enterprise SSO is the only first factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [oktaFactor], + }); + + const { findByText, findByRole, queryByText } = render(, { wrapper }); + + await findByText('Verification required'); + await findByRole('button', { name: /Okta/ }); + expect(queryByText('Use another method')).toBeNull(); + }); + + it('renders one button per enterprise connection', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [oktaFactor, entraFactor], + }); + + const { findByRole } = render(, { wrapper }); + + await findByRole('button', { name: /Okta/ }); + await findByRole('button', { name: /Entra/ }); + }); + + it('lists enterprise SSO under use another method when password is the starting factor', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [{ strategy: 'password' }, oktaFactor], + }); + + const { userEvent, findByText, findByRole, getByLabelText } = render(, { wrapper }); + + await findByText('Verification required'); + getByLabelText(/^password/i); + + await userEvent.click(await findByText('Use another method')); + await userEvent.click(await findByText('Continue with Okta')); + + await findByText('Continue with your enterprise account to confirm it’s you'); + await findByRole('button', { name: /Okta/ }); + }); + + it('redirects the page when the popup is blocked', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [oktaFactor], + }); + fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({ + firstFactorVerification: { externalVerificationRedirectURL: redirectUrl }, + }); + vi.spyOn(window, 'open').mockReturnValue(null); + + const { userEvent, findByRole } = render(, { wrapper }); + + await userEvent.click(await findByRole('button', { name: /Okta/ })); + + await waitFor(() => { + expect(fixtures.session?.prepareFirstFactorVerification).toHaveBeenCalledWith( + expect.objectContaining({ + strategy: 'enterprise_sso', + enterpriseConnectionId: 'ec_1', + redirectUrl: window.location.href, + actionCompleteRedirectUrl: window.location.href, + }), + ); + expect(fixtures.clerk.navigate).toHaveBeenCalledWith(redirectUrl.href); + }); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + }); + + it('sends the email of the enterprise account that belongs to the connection', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ + email_addresses: ['personal@example.com', 'demo@okta-demo.com'], + enterprise_accounts: [ + { + object: 'enterprise_account', + id: 'eac_1', + protocol: 'oauth', + provider: 'oauth_custom_okta', + email_address: 'demo@okta-demo.com', + enterprise_connection_id: '', + enterprise_connection: { + id: 'copc_1', + enterprise_connection_id: '', + protocol: 'oauth', + provider: 'oauth_custom_okta', + name: 'Okta', + domain: 'okta-demo.com', + active: true, + }, + }, + ], + } as any); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [{ ...oktaFactor, enterpriseConnectionProvider: 'oauth_custom_okta' }], + }); + fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({ + firstFactorVerification: { externalVerificationRedirectURL: redirectUrl }, + }); + vi.spyOn(window, 'open').mockReturnValue(null); + + const { userEvent, findByRole } = render(, { wrapper }); + await userEvent.click(await findByRole('button', { name: /Okta/ })); + + const enterpriseEmailId = fixtures.session?.user?.emailAddresses.find( + e => e.emailAddress === 'demo@okta-demo.com', + )?.id; + await waitFor(() => + expect(fixtures.session?.prepareFirstFactorVerification).toHaveBeenCalledWith( + expect.objectContaining({ enterpriseConnectionId: 'ec_1', emailAddressId: enterpriseEmailId }), + ), + ); + }); + + it('shows the API error when preparing the verification fails', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [oktaFactor], + }); + fixtures.session?.prepareFirstFactorVerification.mockRejectedValue( + new ClerkAPIResponseError('Unprocessable', { + status: 422, + data: [ + { + code: 'form_conditional_param_missing', + message: 'is required', + long_message: 'email_address_id is required when strategy is enterprise_sso', + meta: { param_name: 'email_address_id' }, + }, + ], + }), + ); + const popup = { closed: false, close: vi.fn(), location: { href: '' } }; + vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window); + + const { userEvent, findByRole, findByText } = render(, { wrapper }); + await userEvent.click(await findByRole('button', { name: /Okta/ })); + + await findByText(/email_address_id is required/); + expect(popup.close).toHaveBeenCalled(); + }); + + it('redirects the page without opening a popup when oauthFlow is redirect', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + props.setProps({ oauthFlow: 'redirect' }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + supportedFirstFactors: [oktaFactor], + }); + fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({ + firstFactorVerification: { externalVerificationRedirectURL: redirectUrl }, + }); + const open = vi.spyOn(window, 'open'); + + const { userEvent, findByRole } = render(, { wrapper }); + + await userEvent.click(await findByRole('button', { name: /Okta/ })); + + await waitFor(() => expect(fixtures.clerk.navigate).toHaveBeenCalledWith(redirectUrl.href)); + expect(open).not.toHaveBeenCalled(); + }); + + const openPopupAndStart = async ( + fixtures: Awaited>['fixtures'], + wrapper: any, + ) => { + fixtures.session?.prepareFirstFactorVerification.mockResolvedValue({ + firstFactorVerification: { externalVerificationRedirectURL: redirectUrl }, + }); + const popup = { closed: false, close: vi.fn(), location: { href: '' } }; + vi.spyOn(window, 'open').mockReturnValue(popup as unknown as Window); + + const rendered = render(, { wrapper }); + await rendered.userEvent.click(await rendered.findByRole('button', { name: /Okta/ })); + await waitFor(() => expect(popup.location.href).toBe(redirectUrl.href)); + expect(fixtures.clerk.navigate).not.toHaveBeenCalled(); + return { popup, ...rendered }; + }; + + const popupReportsBack = (fixtures: Awaited>['fixtures'], data: object) => + window.dispatchEvent( + new MessageEvent('message', { origin: buildAccountsBaseUrl(fixtures.clerk.frontendApi), data }), + ); + + const sessionVerifiesFirstFactorOnReload = (fixtures: Awaited>['fixtures']) => + vi.spyOn(fixtures.session!, 'reload').mockImplementation(async () => { + fixtures.session!.factorVerificationAge = [0, -1]; + return fixtures.session!; + }); + + it('completes the verification once the popup reports back and the session confirms it', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + level: 'first_factor', + supportedFirstFactors: [oktaFactor], + }); + sessionVerifiesFirstFactorOnReload(fixtures); + + await openPopupAndStart(fixtures, wrapper); + popupReportsBack(fixtures, { session: fixtures.session?.id }); + + await waitFor(() => expect(fixtures.clerk.setActive).toHaveBeenCalledWith({ session: fixtures.session?.id })); + }); + + it('shows an error when the popup reports back but the session is not verified', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + level: 'first_factor', + supportedFirstFactors: [oktaFactor], + }); + fixtures.session!.factorVerificationAge = [-1, -1]; + vi.spyOn(fixtures.session!, 'reload').mockResolvedValue(fixtures.session!); + + const { findByText, getByRole } = await openPopupAndStart(fixtures, wrapper); + popupReportsBack(fixtures, { return_url: 'https://app.example.com' }); + + await findByText(/Verification with your identity provider did not complete/); + expect(getByRole('button', { name: /Okta/ })).toBeEnabled(); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + }); + + it('continues to the second factor when the level is multi_factor', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + props.setProps({ level: 'multi_factor' }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + level: 'multi_factor', + supportedFirstFactors: [oktaFactor], + }); + sessionVerifiesFirstFactorOnReload(fixtures); + + await openPopupAndStart(fixtures, wrapper); + popupReportsBack(fixtures, { session: fixtures.session?.id }); + + await waitFor(() => expect(fixtures.router.navigate).toHaveBeenCalledWith('./factor-two')); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + }); + + it('re-enables the connection button when the popup is closed', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withUser({ username: 'clerkuser' }); + }); + fixtures.session?.startVerification.mockResolvedValue({ + status: 'needs_first_factor', + level: 'first_factor', + supportedFirstFactors: [oktaFactor], + }); + + const { popup, getByRole, queryByText } = await openPopupAndStart(fixtures, wrapper); + popup.closed = true; + + await waitFor(() => expect(getByRole('button', { name: /Okta/ })).toBeEnabled(), { timeout: 3000 }); + expect(popup.close).toHaveBeenCalled(); + expect(queryByText('The popup was closed before the flow completed')).toBeNull(); + expect(fixtures.clerk.setActive).not.toHaveBeenCalled(); + }); + }); + describe('Get Help', () => { it.todo('should render the get help component when clicking the "Get Help" button'); }); diff --git a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts index 9b13966c7b0..1a5dd7e89d4 100644 --- a/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts +++ b/packages/ui/src/components/UserVerification/useReverificationAlternativeStrategies.ts @@ -19,6 +19,10 @@ const firstFactorsAreEqual = (a: SignInFactor | null | undefined, b: SignInFacto return a.phoneNumberId === b.phoneNumberId; } + if (a.strategy === 'enterprise_sso' && b.strategy === 'enterprise_sso') { + return a.enterpriseConnectionId === b.enterpriseConnectionId; + } + return a.strategy === b.strategy; }; @@ -49,7 +53,7 @@ export function useReverificationAlternativeStrategies({ supportedFirstFactors ? (supportedFirstFactors .filter(f => !f.strategy.startsWith('oauth_')) - .filter(factor => factorHasLocalStrategy(factor)) + .filter(factor => factorHasLocalStrategy(factor) || factor.strategy === 'enterprise_sso') .filter(factor => !firstFactorsAreEqual(factor, filterOutFactor)) // Only include passkey if the device supports it. // @ts-ignore Types are not public yet.