diff --git a/.changeset/enterprise-sso-hand-off-challenge.md b/.changeset/enterprise-sso-hand-off-challenge.md
new file mode 100644
index 00000000000..6625e315976
--- /dev/null
+++ b/.changeset/enterprise-sso-hand-off-challenge.md
@@ -0,0 +1,12 @@
+---
+'@clerk/clerk-js': patch
+'@clerk/localizations': patch
+'@clerk/shared': patch
+'@clerk/ui': patch
+---
+
+Fix enterprise SSO sign-ins erroring instead of showing a verification challenge raised while handing off to the identity provider.
+
+If you use the prebuilt `` component, there is nothing to do. If you have Clerk Protect enabled and call `signIn.authenticateWithRedirect()` or `signIn.authenticateWithPopup()` from a custom sign-in flow, catch a `ClerkRuntimeError` with code `protect_check_required` and show the verification challenge, to avoid a stalled sign-in.
+
+That error means a verification challenge has to be completed before the sign-in can redirect. It replaces the generic "not supported" error these methods threw before. When it is thrown, the sign-in is gated: `signIn.protectCheck` is set, or its status is `needs_protect_check`. For enterprise SSO, run the challenge and then call `authenticateWithRedirect()` again with `continueSignIn: true`. If the server has already prepared the redirect, the sign-in continues to the identity provider and the challenge runs when it returns, so no error is thrown.
diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts
index d6369a138a8..8f3e8ab0414 100644
--- a/packages/clerk-js/src/core/resources/SignIn.ts
+++ b/packages/clerk-js/src/core/resources/SignIn.ts
@@ -1,5 +1,6 @@
import { inBrowser } from '@clerk/shared/browser';
import { type ClerkError, ClerkRuntimeError, ClerkWebAuthnError } from '@clerk/shared/error';
+import { ERROR_CODES } from '@clerk/shared/internal/clerk-js/constants';
import {
convertJSONToPublicKeyRequestOptions,
serializePublicKeyCredentialAssertion,
@@ -389,6 +390,27 @@ export class SignIn extends BaseResource implements SignInResource {
const redirectUrl = SignIn.clerk.buildUrlWithAuth(params.redirectUrl);
+ const isChallengePending = () => !!this.protectCheck || this.status === 'needs_protect_check';
+ const pendingHandOff = () => {
+ const { status, externalVerificationRedirectURL } = this.firstFactorVerification;
+ return status === 'unverified' ? externalVerificationRedirectURL : null;
+ };
+
+ // A pending challenge with nowhere to navigate to. Throw rather than return, so the method still
+ // either navigates or throws: a caller that doesn't handle challenges gets an error it can
+ // recognise instead of a silent success. A caller that does runs the challenge and calls back
+ // in with `continueSignIn`.
+ const throwChallengeRequired = (): never => {
+ throw new ClerkRuntimeError('A verification challenge must be completed before this sign-in can continue.', {
+ code: ERROR_CODES.PROTECT_CHECK_REQUIRED,
+ });
+ };
+
+ // The hand-off a challenged create built, if any. The server builds it before deciding, so a
+ // challenge on create can arrive with a usable redirect: that means "go to the identity
+ // provider first" and the challenge runs on the way back, where the callback routes to it.
+ let challengedCreateHandOff: URL | null = null;
+
if (!this.id || !continueSignIn) {
await this.create({
strategy,
@@ -396,6 +418,13 @@ export class SignIn extends BaseResource implements SignInResource {
redirectUrl,
actionCompleteRedirectUrl,
});
+
+ if (isChallengePending()) {
+ challengedCreateHandOff = pendingHandOff();
+ if (!challengedCreateHandOff) {
+ throwChallengeRequired();
+ }
+ }
}
if (strategy === 'enterprise_sso') {
@@ -406,6 +435,17 @@ export class SignIn extends BaseResource implements SignInResource {
oidcPrompt,
enterpriseConnectionId,
});
+
+ // A challenged prepare builds no verification, so any redirect left on the sign-in is from an
+ // earlier attempt and may be for another connection. Only this call's create hand-off is safe
+ // to follow.
+ if (isChallengePending()) {
+ if (challengedCreateHandOff) {
+ navigateCallback(challengedCreateHandOff);
+ return;
+ }
+ throwChallengeRequired();
+ }
}
const { status, externalVerificationRedirectURL } = this.firstFactorVerification;
diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
index 120aafdc753..374b11163a1 100644
--- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
+++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
@@ -311,6 +311,189 @@ describe('SignIn', () => {
});
});
+ describe('authenticateWithRedirect with a pending challenge', () => {
+ const originalFetch = BaseResource._fetch;
+
+ afterEach(() => {
+ BaseResource._fetch = originalFetch;
+ vi.clearAllMocks();
+ SignIn.clerk = {} as any;
+ });
+
+ const gatedResponse = {
+ client: null,
+ response: {
+ id: 'signin_123',
+ status: 'needs_protect_check',
+ first_factor_verification: null,
+ protect_check: {
+ status: 'pending',
+ token: 'challenge-token-abc',
+ sdk_url: 'https://sdk.example.com/challenge.js',
+ },
+ },
+ };
+
+ const setupClerk = () => {
+ const windowNavigate = vi.fn();
+ SignIn.clerk = {
+ buildUrlWithAuth: vi.fn(u => u),
+ __internal_windowNavigate: windowNavigate,
+ __internal_environment: { displayConfig: { captchaOauthBypass: [] } },
+ } as any;
+ return windowNavigate;
+ };
+
+ // The server builds the hand-off before it decides, so a challenged create can also carry a
+ // usable redirect.
+ const gatedWithHandOff = (url: string) => ({
+ client: null,
+ response: {
+ ...gatedResponse.response,
+ first_factor_verification: { status: 'unverified', external_verification_redirect_url: url },
+ },
+ });
+
+ it('follows the hand-off a challenged OAuth create built, leaving the challenge for the way back', async () => {
+ const windowNavigate = setupClerk();
+ const mockFetch = vi.fn().mockResolvedValue(gatedWithHandOff('https://accounts.google.example/auth'));
+ BaseResource._fetch = mockFetch;
+
+ const signIn = new SignIn();
+ await signIn.authenticateWithRedirect({
+ strategy: 'oauth_google',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ });
+
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(windowNavigate).toHaveBeenCalledWith(new URL('https://accounts.google.example/auth'));
+ });
+
+ it('follows the create hand-off when the enterprise SSO prepare after it hits the same pending challenge', async () => {
+ const windowNavigate = setupClerk();
+ // Create builds the hand-off and is challenged; the prepare that follows lands on the same
+ // pending gate and builds nothing new.
+ const mockFetch = vi.fn().mockResolvedValue(gatedWithHandOff('https://idp.example/from-create'));
+ BaseResource._fetch = mockFetch;
+
+ const signIn = new SignIn();
+ await signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ });
+
+ expect(mockFetch).toHaveBeenCalledTimes(2);
+ expect(windowNavigate).toHaveBeenCalledWith(new URL('https://idp.example/from-create'));
+ });
+
+ it('does not follow a hand-off left from an earlier attempt when the prepare is challenged', async () => {
+ const windowNavigate = setupClerk();
+ // The challenged prepare builds no verification, so the redirect on the sign-in is stale and
+ // may be for a different connection.
+ BaseResource._fetch = vi.fn().mockResolvedValue(gatedWithHandOff('https://idp.example/earlier-attempt'));
+
+ const signIn = new SignIn({ id: 'signin_123' } as any);
+ await expect(
+ signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ continueSignIn: true,
+ enterpriseConnectionId: 'ent_other',
+ }),
+ ).rejects.toMatchObject({ code: 'protect_check_required' });
+
+ expect(windowNavigate).not.toHaveBeenCalled();
+ });
+
+ it('throws protect_check_required when a challenged create built no hand-off', async () => {
+ const windowNavigate = setupClerk();
+ const mockFetch = vi.fn().mockResolvedValue(gatedResponse);
+ BaseResource._fetch = mockFetch;
+
+ const signIn = new SignIn();
+ await expect(
+ signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ }),
+ ).rejects.toMatchObject({ code: 'protect_check_required' });
+
+ // Only the create call — the prepare is not attempted while the challenge is pending.
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ expect(windowNavigate).not.toHaveBeenCalled();
+ expect(signIn.protectCheck?.status).toBe('pending');
+ });
+
+ it('throws protect_check_required when preparing the enterprise SSO hand-off returns a challenge', async () => {
+ const windowNavigate = setupClerk();
+ const mockFetch = vi.fn().mockResolvedValue(gatedResponse);
+ BaseResource._fetch = mockFetch;
+
+ const signIn = new SignIn({ id: 'signin_123' } as any);
+ await expect(
+ signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ continueSignIn: true,
+ }),
+ ).rejects.toMatchObject({ code: 'protect_check_required' });
+
+ expect(windowNavigate).not.toHaveBeenCalled();
+ expect(signIn.protectCheck?.status).toBe('pending');
+ });
+
+ it('surfaces protect_check_required through an OAuth transport instead of opening it', async () => {
+ // The transport expects a URL back. Returning without one used to surface as
+ // `oauth_transport_missing_verification_url`, which hid the real reason.
+ setupClerk();
+ const transport = { getRedirectUrl: vi.fn().mockResolvedValue('app://callback'), open: vi.fn() };
+ (SignIn.clerk as any).__internal_oauthTransport = transport;
+ BaseResource._fetch = vi.fn().mockResolvedValue(gatedResponse);
+
+ const signIn = new SignIn({ id: 'signin_123' } as any);
+ await expect(
+ signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ continueSignIn: true,
+ }),
+ ).rejects.toMatchObject({ code: 'protect_check_required' });
+
+ expect(transport.open).not.toHaveBeenCalled();
+ });
+
+ it('follows the hand-off once no challenge is pending', async () => {
+ const windowNavigate = setupClerk();
+ BaseResource._fetch = vi.fn().mockResolvedValue({
+ client: null,
+ response: {
+ id: 'signin_123',
+ status: 'needs_first_factor',
+ first_factor_verification: {
+ status: 'unverified',
+ external_verification_redirect_url: 'https://idp.example/auth',
+ },
+ },
+ });
+
+ const signIn = new SignIn({ id: 'signin_123' } as any);
+ await signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/',
+ continueSignIn: true,
+ });
+
+ expect(windowNavigate).toHaveBeenCalledWith(new URL('https://idp.example/auth'));
+ });
+ });
+
describe('signIn.create', () => {
afterEach(() => {
vi.clearAllMocks();
diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts
index b170384b18e..79464cff73d 100644
--- a/packages/localizations/src/ar-SA.ts
+++ b/packages/localizations/src/ar-SA.ts
@@ -2061,6 +2061,7 @@ export const arSA: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts
index a487e4c1a91..0499ceeee98 100644
--- a/packages/localizations/src/be-BY.ts
+++ b/packages/localizations/src/be-BY.ts
@@ -2072,6 +2072,7 @@ export const beBY: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts
index 5667ae5a7b5..c3202e61755 100644
--- a/packages/localizations/src/bg-BG.ts
+++ b/packages/localizations/src/bg-BG.ts
@@ -2065,6 +2065,7 @@ export const bgBG: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts
index 709c93aaebe..a5e177ae7dd 100644
--- a/packages/localizations/src/bn-IN.ts
+++ b/packages/localizations/src/bn-IN.ts
@@ -2090,6 +2090,7 @@ export const bnIN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts
index cb05017c14f..be31010d7ed 100644
--- a/packages/localizations/src/ca-ES.ts
+++ b/packages/localizations/src/ca-ES.ts
@@ -2078,6 +2078,7 @@ export const caES: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts
index b996c0ce0fc..018f7b4a436 100644
--- a/packages/localizations/src/cs-CZ.ts
+++ b/packages/localizations/src/cs-CZ.ts
@@ -2077,6 +2077,7 @@ export const csCZ: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts
index 29e042758f5..a34d0a620d6 100644
--- a/packages/localizations/src/da-DK.ts
+++ b/packages/localizations/src/da-DK.ts
@@ -2062,6 +2062,7 @@ export const daDK: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts
index b44f723fb29..f9b838ef47e 100644
--- a/packages/localizations/src/de-DE.ts
+++ b/packages/localizations/src/de-DE.ts
@@ -2095,6 +2095,7 @@ export const deDE: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts
index 3c2456d3edd..50f0672c06e 100644
--- a/packages/localizations/src/el-GR.ts
+++ b/packages/localizations/src/el-GR.ts
@@ -2086,6 +2086,7 @@ export const elGR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts
index c35c73fb1fd..f41dfdd3265 100644
--- a/packages/localizations/src/en-GB.ts
+++ b/packages/localizations/src/en-GB.ts
@@ -2069,6 +2069,7 @@ export const enGB: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index 367ccc7fd4b..0ec04871220 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -2118,6 +2118,8 @@ export const enUS: LocalizationResource = {
protect_check_execution_failed: "Verification didn't complete. Please try again.",
protect_check_invalid_script: "Couldn't load verification. Please contact support if this persists.",
protect_check_invalid_sdk_url: "Verification couldn't start. Please contact support.",
+ protect_check_required:
+ "This sign-in needs an extra verification step that can't be shown here. Please try again or use a different sign-in method.",
protect_check_script_load_failed:
"Couldn't load verification. This may be caused by a network issue or a Content Security Policy that blocks the verification script. Please try again or contact support.",
protect_check_timed_out: "Verification didn't complete in time. Please try again.",
diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts
index 206c578c1d8..0d8ea7143cf 100644
--- a/packages/localizations/src/es-CR.ts
+++ b/packages/localizations/src/es-CR.ts
@@ -2077,6 +2077,7 @@ export const esCR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts
index 4cf19bd234f..4ef9b4bd31b 100644
--- a/packages/localizations/src/es-ES.ts
+++ b/packages/localizations/src/es-ES.ts
@@ -2078,6 +2078,7 @@ export const esES: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts
index f70472db2a7..0f71e3df409 100644
--- a/packages/localizations/src/es-MX.ts
+++ b/packages/localizations/src/es-MX.ts
@@ -2078,6 +2078,7 @@ export const esMX: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts
index f9dd35007b5..398fe3a806c 100644
--- a/packages/localizations/src/es-UY.ts
+++ b/packages/localizations/src/es-UY.ts
@@ -2079,6 +2079,7 @@ export const esUY: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts
index 7922218161c..69f31d17907 100644
--- a/packages/localizations/src/fa-IR.ts
+++ b/packages/localizations/src/fa-IR.ts
@@ -2076,6 +2076,7 @@ export const faIR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts
index 4cf9bc33f64..92673e1d8fd 100644
--- a/packages/localizations/src/fi-FI.ts
+++ b/packages/localizations/src/fi-FI.ts
@@ -2092,6 +2092,7 @@ export const fiFI: LocalizationResource = {
protect_check_execution_failed: 'Tarkistus ei valmistunut. Yritä uudelleen.',
protect_check_invalid_script: 'Tarkistusta ei voitu ladata. Ota yhteyttä tukeen, jos ongelma jatkuu.',
protect_check_invalid_sdk_url: 'Tarkistusta ei voitu käynnistää. Ota yhteyttä tukeen.',
+ protect_check_required: undefined,
protect_check_script_load_failed:
'Tarkistusta ei voitu ladata. Syynä voi olla verkkoyhteys tai sisällön suojauskäytäntö, joka estää tarkistuksen skriptin. Yritä uudelleen tai ota yhteyttä tukeen.',
protect_check_timed_out: 'Tarkistus ei valmistunut ajoissa. Yritä uudelleen.',
diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts
index da5192566a5..d0da663e7f1 100644
--- a/packages/localizations/src/fr-FR.ts
+++ b/packages/localizations/src/fr-FR.ts
@@ -2087,6 +2087,7 @@ export const frFR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts
index 93e2b829ee1..620cf66b1e7 100644
--- a/packages/localizations/src/he-IL.ts
+++ b/packages/localizations/src/he-IL.ts
@@ -2053,6 +2053,7 @@ export const heIL: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts
index b916ac42fcd..36678670448 100644
--- a/packages/localizations/src/hi-IN.ts
+++ b/packages/localizations/src/hi-IN.ts
@@ -2091,6 +2091,7 @@ export const hiIN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts
index 724ffa36afd..0dc97826c30 100644
--- a/packages/localizations/src/hr-HR.ts
+++ b/packages/localizations/src/hr-HR.ts
@@ -2096,6 +2096,7 @@ export const hrHR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts
index 1a706959464..68784c5f74b 100644
--- a/packages/localizations/src/hu-HU.ts
+++ b/packages/localizations/src/hu-HU.ts
@@ -2095,6 +2095,7 @@ export const huHU: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts
index 95e2115583f..0d7501697e3 100644
--- a/packages/localizations/src/id-ID.ts
+++ b/packages/localizations/src/id-ID.ts
@@ -2075,6 +2075,7 @@ export const idID: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts
index acdb6e234ec..447a608a5ae 100644
--- a/packages/localizations/src/is-IS.ts
+++ b/packages/localizations/src/is-IS.ts
@@ -2093,6 +2093,7 @@ export const isIS: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts
index 93fb06cc64a..85c063ec9e5 100644
--- a/packages/localizations/src/it-IT.ts
+++ b/packages/localizations/src/it-IT.ts
@@ -2074,6 +2074,7 @@ export const itIT: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts
index c9064dccb59..4f35db7734a 100644
--- a/packages/localizations/src/ja-JP.ts
+++ b/packages/localizations/src/ja-JP.ts
@@ -2089,6 +2089,7 @@ export const jaJP: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts
index f6bfd105eff..46121895fee 100644
--- a/packages/localizations/src/kk-KZ.ts
+++ b/packages/localizations/src/kk-KZ.ts
@@ -2055,6 +2055,7 @@ export const kkKZ: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts
index 8f897540d61..03da92dd60f 100644
--- a/packages/localizations/src/ko-KR.ts
+++ b/packages/localizations/src/ko-KR.ts
@@ -2062,6 +2062,7 @@ export const koKR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts
index 22628699d22..ff13835bc24 100644
--- a/packages/localizations/src/mn-MN.ts
+++ b/packages/localizations/src/mn-MN.ts
@@ -2067,6 +2067,7 @@ export const mnMN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts
index 9e6f2196d2c..ec8f6d5ebba 100644
--- a/packages/localizations/src/ms-MY.ts
+++ b/packages/localizations/src/ms-MY.ts
@@ -2100,6 +2100,7 @@ export const msMY: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts
index 656d86c9abd..cedb85df992 100644
--- a/packages/localizations/src/nb-NO.ts
+++ b/packages/localizations/src/nb-NO.ts
@@ -2093,6 +2093,7 @@ export const nbNO: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts
index 058d0566e3f..cbe40324bdf 100644
--- a/packages/localizations/src/nl-BE.ts
+++ b/packages/localizations/src/nl-BE.ts
@@ -2066,6 +2066,7 @@ export const nlBE: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts
index a51c1995653..81c6dc0e1f0 100644
--- a/packages/localizations/src/nl-NL.ts
+++ b/packages/localizations/src/nl-NL.ts
@@ -2066,6 +2066,7 @@ export const nlNL: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts
index 24c93f5909b..104d1190f30 100644
--- a/packages/localizations/src/pl-PL.ts
+++ b/packages/localizations/src/pl-PL.ts
@@ -2074,6 +2074,7 @@ export const plPL: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts
index ff44fd059d6..4e3f15b9ea2 100644
--- a/packages/localizations/src/pt-BR.ts
+++ b/packages/localizations/src/pt-BR.ts
@@ -2084,6 +2084,7 @@ export const ptBR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts
index d9b75a842ac..6d6200408f7 100644
--- a/packages/localizations/src/pt-PT.ts
+++ b/packages/localizations/src/pt-PT.ts
@@ -2090,6 +2090,7 @@ export const ptPT: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts
index 3af154f9b57..d8f2a12cfe0 100644
--- a/packages/localizations/src/ro-RO.ts
+++ b/packages/localizations/src/ro-RO.ts
@@ -2085,6 +2085,7 @@ export const roRO: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts
index 2e8589012b4..7a2630db832 100644
--- a/packages/localizations/src/ru-RU.ts
+++ b/packages/localizations/src/ru-RU.ts
@@ -2082,6 +2082,7 @@ export const ruRU: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts
index 8a7fa661be9..f17f98c54a1 100644
--- a/packages/localizations/src/sk-SK.ts
+++ b/packages/localizations/src/sk-SK.ts
@@ -2073,6 +2073,7 @@ export const skSK: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts
index 3cb05958a6f..27654d0c457 100644
--- a/packages/localizations/src/sr-RS.ts
+++ b/packages/localizations/src/sr-RS.ts
@@ -2065,6 +2065,7 @@ export const srRS: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts
index d30a15de531..a0805ff84ae 100644
--- a/packages/localizations/src/sv-SE.ts
+++ b/packages/localizations/src/sv-SE.ts
@@ -2068,6 +2068,7 @@ export const svSE: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts
index b8c369c0280..bb91d5af794 100644
--- a/packages/localizations/src/ta-IN.ts
+++ b/packages/localizations/src/ta-IN.ts
@@ -2101,6 +2101,7 @@ export const taIN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts
index cf3e3b183ec..2cbe9dd2e0d 100644
--- a/packages/localizations/src/te-IN.ts
+++ b/packages/localizations/src/te-IN.ts
@@ -2095,6 +2095,7 @@ export const teIN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts
index 83b15833bb5..28bd03c214e 100644
--- a/packages/localizations/src/th-TH.ts
+++ b/packages/localizations/src/th-TH.ts
@@ -2063,6 +2063,7 @@ export const thTH: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts
index a09e323e061..73d3d25c826 100644
--- a/packages/localizations/src/tr-TR.ts
+++ b/packages/localizations/src/tr-TR.ts
@@ -2069,6 +2069,7 @@ export const trTR: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts
index 5b5283e1b6c..f590cf5557d 100644
--- a/packages/localizations/src/uk-UA.ts
+++ b/packages/localizations/src/uk-UA.ts
@@ -2063,6 +2063,7 @@ export const ukUA: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts
index 028d41cf23d..5cb8bc9b8dd 100644
--- a/packages/localizations/src/vi-VN.ts
+++ b/packages/localizations/src/vi-VN.ts
@@ -2086,6 +2086,7 @@ export const viVN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts
index 2c3ee135aa3..bed7dffb089 100644
--- a/packages/localizations/src/zh-CN.ts
+++ b/packages/localizations/src/zh-CN.ts
@@ -2046,6 +2046,7 @@ export const zhCN: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts
index 7cbca618cb1..e5275233143 100644
--- a/packages/localizations/src/zh-TW.ts
+++ b/packages/localizations/src/zh-TW.ts
@@ -2051,6 +2051,7 @@ export const zhTW: LocalizationResource = {
protect_check_execution_failed: undefined,
protect_check_invalid_script: undefined,
protect_check_invalid_sdk_url: undefined,
+ protect_check_required: undefined,
protect_check_script_load_failed: undefined,
protect_check_timed_out: undefined,
protect_check_unsupported_environment: undefined,
diff --git a/packages/shared/src/internal/clerk-js/constants.ts b/packages/shared/src/internal/clerk-js/constants.ts
index c11db68f590..b87fbaa578f 100644
--- a/packages/shared/src/internal/clerk-js/constants.ts
+++ b/packages/shared/src/internal/clerk-js/constants.ts
@@ -47,6 +47,7 @@ export const ERROR_CODES = {
FRAUD_DEVICE_BLOCKED: 'device_blocked',
FRAUD_ACTION_BLOCKED: 'action_blocked',
PROTECT_CHECK_ALREADY_RESOLVED: 'protect_check_already_resolved',
+ PROTECT_CHECK_REQUIRED: 'protect_check_required',
PROTECT_CHECK_TIMED_OUT: 'protect_check_timed_out',
PROTECT_CHECK_UNSUPPORTED_ENVIRONMENT: 'protect_check_unsupported_environment',
SIGNUP_RATE_LIMIT_EXCEEDED: 'signup_rate_limit_exceeded',
diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts
index 16c2296d41f..15eaa32b173 100644
--- a/packages/shared/src/types/localization.ts
+++ b/packages/shared/src/types/localization.ts
@@ -2410,6 +2410,7 @@ type UnstableErrors = WithParamName<{
protect_check_execution_failed: LocalizationValue;
protect_check_invalid_script: LocalizationValue;
protect_check_invalid_sdk_url: LocalizationValue;
+ protect_check_required: LocalizationValue;
protect_check_script_load_failed: LocalizationValue;
protect_check_timed_out: LocalizationValue;
protect_check_unsupported_environment: LocalizationValue;
diff --git a/packages/ui/src/components/SignIn/SignInFactorOneSSOBypass.tsx b/packages/ui/src/components/SignIn/SignInFactorOneSSOBypass.tsx
index 770f60ce70f..5f9072e411e 100644
--- a/packages/ui/src/components/SignIn/SignInFactorOneSSOBypass.tsx
+++ b/packages/ui/src/components/SignIn/SignInFactorOneSSOBypass.tsx
@@ -9,6 +9,8 @@ import { handleError } from '@/ui/utils/errorHandler';
import { useCoreSignIn, useSignInContext } from '../../contexts';
import { Button, Col, descriptors, Flow, localizationKeys } from '../../customizables';
+import { useRouter } from '../../router';
+import { isProtectCheckRequiredError, navigateOnSignInProtectGate } from './handleProtectCheck';
import { hasMultipleEnterpriseConnections } from './shared';
import { SignInFactorOneCodeForm } from './SignInFactorOneCodeForm';
@@ -30,6 +32,7 @@ export const SignInFactorOneSSOBypass = (props: SignInFactorOneSSOBypassProps) =
const card = useCardState();
const ctx = useSignInContext();
const signIn = useCoreSignIn();
+ const { navigate } = useRouter();
const [step, setStep] = React.useState('sso');
const [isRedirecting, setIsRedirecting] = React.useState(false);
@@ -39,14 +42,23 @@ export const SignInFactorOneSSOBypass = (props: SignInFactorOneSSOBypassProps) =
};
const authenticateWithEnterpriseSSO = async (enterpriseConnectionId?: string) => {
- await signIn.authenticateWithRedirect({
- strategy: 'enterprise_sso',
- redirectUrl: ctx.ssoCallbackUrl,
- redirectUrlComplete: ctx.afterSignInUrl || '/',
- oidcPrompt: ctx.oidcPrompt,
- continueSignIn: true,
- ...(enterpriseConnectionId && { enterpriseConnectionId }),
- });
+ try {
+ await signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl: ctx.ssoCallbackUrl,
+ redirectUrlComplete: ctx.afterSignInUrl || '/',
+ oidcPrompt: ctx.oidcPrompt,
+ continueSignIn: true,
+ ...(enterpriseConnectionId && { enterpriseConnectionId }),
+ });
+ } catch (err) {
+ // Preparing the hand-off can itself raise a challenge. No redirect was issued and the sign-in
+ // is sitting on the gate instead: run the challenge rather than showing it as an error.
+ if (isProtectCheckRequiredError(err) && navigateOnSignInProtectGate(signIn, navigate, '../protect-check')) {
+ return;
+ }
+ throw err;
+ }
};
const handleSSOError = (err: Error) => handleError(err, [], card.setError);
diff --git a/packages/ui/src/components/SignIn/SignInStart.tsx b/packages/ui/src/components/SignIn/SignInStart.tsx
index 5e55b7223c6..3349ae5d541 100644
--- a/packages/ui/src/components/SignIn/SignInStart.tsx
+++ b/packages/ui/src/components/SignIn/SignInStart.tsx
@@ -39,7 +39,7 @@ import { useSupportEmail } from '../../hooks/useSupportEmail';
import { useTotalEnabledAuthMethods } from '../../hooks/useTotalEnabledAuthMethods';
import { useRouter } from '../../router';
import { handleCombinedFlowTransfer } from './handleCombinedFlowTransfer';
-import { navigateOnSignInProtectGate } from './handleProtectCheck';
+import { isProtectCheckRequiredError, navigateOnSignInProtectGate } from './handleProtectCheck';
import {
getSSOBypassFactor,
hasMultipleEnterpriseConnections,
@@ -428,7 +428,8 @@ function SignInStartInternal(): JSX.Element {
return navigate('factor-one');
}
- return authenticateWithEnterpriseSSO();
+ // Awaited so a failed hand-off reaches the catch below instead of escaping this try.
+ return await authenticateWithEnterpriseSSO();
}
case 'needs_second_factor':
return navigate('factor-two');
@@ -455,13 +456,23 @@ function SignInStartInternal(): JSX.Element {
const redirectUrl = ctx.ssoCallbackUrl;
const redirectUrlComplete = ctx.afterSignInUrl || '/';
- return signIn.authenticateWithRedirect({
- strategy: 'enterprise_sso',
- redirectUrl,
- redirectUrlComplete,
- oidcPrompt: ctx.oidcPrompt,
- continueSignIn: true,
- });
+ try {
+ await signIn.authenticateWithRedirect({
+ strategy: 'enterprise_sso',
+ redirectUrl,
+ redirectUrlComplete,
+ oidcPrompt: ctx.oidcPrompt,
+ continueSignIn: true,
+ });
+ } catch (err) {
+ // Preparing the hand-off can itself raise a challenge. No redirect was issued and the sign-in
+ // is sitting on the gate instead. Handled here because the callers' recovery path drops
+ // errors that didn't come from the API.
+ if (isProtectCheckRequiredError(err) && navigateOnSignInProtectGate(signIn, navigate, 'protect-check')) {
+ return;
+ }
+ throw err;
+ }
};
const attemptToRecoverFromSignInError = async (e: any) => {
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInFactorOneSSOBypass.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInFactorOneSSOBypass.test.tsx
index ce14de6e081..10bceebcec0 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInFactorOneSSOBypass.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInFactorOneSSOBypass.test.tsx
@@ -1,3 +1,4 @@
+import { ClerkRuntimeError } from '@clerk/shared/error';
import type { SignInResource } from '@clerk/shared/types';
import { describe, expect, it } from 'vitest';
@@ -66,6 +67,24 @@ describe('SignInFactorOne SSO bypass', () => {
);
});
+ it('routes to the challenge when preparing the hand-off raises one', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withEmailAddress();
+ f.startSignInWithEnterpriseSSO({ supportSSOBypass: true });
+ });
+ // No redirect is issued: the sign-in comes back sitting on the challenge and the call throws.
+ fixtures.signIn.authenticateWithRedirect.mockImplementationOnce(() => {
+ (fixtures.signIn as any).protectCheck = { status: 'pending', token: 'challenge-token-abc' };
+ throw new ClerkRuntimeError('challenge required', { code: 'protect_check_required' });
+ });
+
+ const { userEvent } = render(, { wrapper });
+ await userEvent.click(await screen.findByText('Continue with SSO'));
+
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('../protect-check');
+ expect(screen.queryByText(/needs an extra verification step/i)).not.toBeInTheDocument();
+ });
+
it('prepares the email code with the handle and warns on the code screen', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withEmailAddress();
diff --git a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
index 7c26db967fb..1da263fbd58 100644
--- a/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
+++ b/packages/ui/src/components/SignIn/__tests__/SignInStart.test.tsx
@@ -1,4 +1,4 @@
-import { ClerkAPIResponseError, ClerkWebAuthnError } from '@clerk/shared/error';
+import { ClerkAPIResponseError, ClerkRuntimeError, ClerkWebAuthnError } from '@clerk/shared/error';
import { CAPTCHA_ELEMENT_ID } from '@clerk/shared/internal/clerk-js/constants';
import { OAUTH_PROVIDERS } from '@clerk/shared/oauth';
import type { SignInResource } from '@clerk/shared/types';
@@ -394,6 +394,23 @@ describe('SignInStart', () => {
});
});
});
+
+ it('explains a challenge the social button cannot run, instead of showing the raw error', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withSocialProvider({ provider: 'google' });
+ });
+ fixtures.signIn.authenticateWithRedirect.mockRejectedValueOnce(
+ new ClerkRuntimeError('A verification challenge must be completed before this sign-in can continue.', {
+ code: 'protect_check_required',
+ }),
+ );
+
+ const { userEvent } = render(, { wrapper });
+ await userEvent.click(screen.getByText('Continue with Google'));
+
+ expect(await screen.findByText(/needs an extra verification step/i)).toBeInTheDocument();
+ expect(screen.queryByText(/code="protect_check_required"/)).not.toBeInTheDocument();
+ });
});
describe('navigation', () => {
@@ -496,6 +513,49 @@ describe('SignInStart', () => {
expect(fixtures.signIn.authenticateWithRedirect).not.toHaveBeenCalled();
expect(fixtures.router.navigate).toHaveBeenCalledWith('factor-one');
});
+
+ it('routes to the challenge when preparing the hand-off raises one', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withEmailAddress();
+ });
+ fixtures.signIn.create.mockReturnValueOnce(
+ Promise.resolve({
+ status: 'needs_first_factor',
+ supportedFirstFactors: [{ strategy: 'enterprise_sso' }],
+ } as unknown as SignInResource),
+ );
+ // No redirect is issued: the sign-in comes back sitting on the challenge and the call throws.
+ fixtures.signIn.authenticateWithRedirect.mockImplementationOnce(async () => {
+ (fixtures.signIn as any).protectCheck = { status: 'pending', token: 'challenge-token-abc' };
+ throw new ClerkRuntimeError('challenge required', { code: 'protect_check_required' });
+ });
+ const { userEvent } = render(, { wrapper });
+ await userEvent.type(screen.getByLabelText(/email address/i), 'hello@clerk.com');
+ await userEvent.click(screen.getByText('Continue'));
+ expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalled();
+ expect(fixtures.router.navigate).toHaveBeenCalledWith('protect-check');
+ });
+
+ it('does not route to the challenge for other hand-off errors', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withEmailAddress();
+ });
+ fixtures.signIn.create.mockReturnValueOnce(
+ Promise.resolve({
+ status: 'needs_first_factor',
+ supportedFirstFactors: [{ strategy: 'enterprise_sso' }],
+ } as unknown as SignInResource),
+ );
+ fixtures.signIn.authenticateWithRedirect.mockImplementationOnce(async () => {
+ (fixtures.signIn as any).protectCheck = { status: 'pending', token: 'challenge-token-abc' };
+ throw new ClerkRuntimeError('something else', { code: 'captcha_unavailable' });
+ });
+ const { userEvent } = render(, { wrapper });
+ await userEvent.type(screen.getByLabelText(/email address/i), 'hello@clerk.com');
+ await userEvent.click(screen.getByText('Continue'));
+ expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalled();
+ expect(fixtures.router.navigate).not.toHaveBeenCalledWith('protect-check');
+ });
});
describe('Identifier switching', () => {
diff --git a/packages/ui/src/components/SignIn/__tests__/handleProtectCheck.test.ts b/packages/ui/src/components/SignIn/__tests__/handleProtectCheck.test.ts
index 154bd70731d..63d6e306358 100644
--- a/packages/ui/src/components/SignIn/__tests__/handleProtectCheck.test.ts
+++ b/packages/ui/src/components/SignIn/__tests__/handleProtectCheck.test.ts
@@ -1,7 +1,8 @@
+import { ClerkAPIResponseError, ClerkRuntimeError } from '@clerk/shared/error';
import type { ProtectCheckResource, SignInResource } from '@clerk/shared/types';
import { describe, expect, it, vi } from 'vitest';
-import { isSignInProtectGated, navigateOnSignInProtectGate } from '../handleProtectCheck';
+import { isProtectCheckRequiredError, isSignInProtectGated, navigateOnSignInProtectGate } from '../handleProtectCheck';
const PENDING_CHECK: ProtectCheckResource = {
status: 'pending',
@@ -25,6 +26,22 @@ describe('isSignInProtectGated', () => {
});
});
+describe('isProtectCheckRequiredError', () => {
+ it('is true for the runtime error authenticateWithRedirect throws on a pending challenge', () => {
+ expect(isProtectCheckRequiredError(new ClerkRuntimeError('x', { code: 'protect_check_required' }))).toBe(true);
+ });
+
+ it('is false for other runtime errors, API errors and non-errors', () => {
+ expect(isProtectCheckRequiredError(new ClerkRuntimeError('x', { code: 'captcha_unavailable' }))).toBe(false);
+ expect(
+ isProtectCheckRequiredError(
+ new ClerkAPIResponseError('x', { data: [{ code: 'protect_check_required', message: 'x' }], status: 400 }),
+ ),
+ ).toBe(false);
+ expect(isProtectCheckRequiredError(undefined)).toBe(false);
+ });
+});
+
describe('navigateOnSignInProtectGate', () => {
it('navigates to the provided path and returns true when gated by the protectCheck field', () => {
const navigate = vi.fn().mockResolvedValue(undefined);
diff --git a/packages/ui/src/components/SignIn/handleProtectCheck.ts b/packages/ui/src/components/SignIn/handleProtectCheck.ts
index 71004dd29b5..3936927b235 100644
--- a/packages/ui/src/components/SignIn/handleProtectCheck.ts
+++ b/packages/ui/src/components/SignIn/handleProtectCheck.ts
@@ -1,3 +1,5 @@
+import { isClerkRuntimeError } from '@clerk/shared/error';
+import { ERROR_CODES } from '@clerk/shared/internal/clerk-js/constants';
import type { SignInResource } from '@clerk/shared/types';
/**
@@ -37,6 +39,19 @@ export function navigateOnSignInProtectGate(
return false;
}
+/**
+ * Whether `err` is the error `authenticateWithRedirect` throws when a challenge stopped it before it
+ * could redirect. The sign-in has already been updated and is sitting on the gate, so the caller
+ * routes to the challenge rather than showing the error.
+ */
+export function isProtectCheckRequiredError(err: unknown): boolean {
+ // The type guard throws on a non-object, and a catch block can receive anything.
+ if (typeof err !== 'object' || err === null) {
+ return false;
+ }
+ return isClerkRuntimeError(err) && err.code === ERROR_CODES.PROTECT_CHECK_REQUIRED;
+}
+
/**
* Whether this sign-in is waiting to become a sign-up.
*/