diff --git a/.changeset/curly-lemons-rhyme.md b/.changeset/curly-lemons-rhyme.md new file mode 100644 index 00000000000..8d11da2898f --- /dev/null +++ b/.changeset/curly-lemons-rhyme.md @@ -0,0 +1,5 @@ +--- +"@clerk/elements": patch +--- + +Fixes issue where the incorrect sign in first factor strategy was being returned during sign in. diff --git a/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts b/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts index adb724898a1..62e424d9a38 100644 --- a/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts +++ b/packages/elements/src/internals/machines/sign-in/utils/starting-factors.ts @@ -1,11 +1,53 @@ // These utilities are ported from: packages/clerk-js/src/ui/components/SignIn/utils.ts // They should be functionally identical. import { isWebAuthnSupported } from '@clerk/shared/webauthn'; -import type { PreferredSignInStrategy, SignInFactor, SignInFirstFactor, SignInSecondFactor } from '@clerk/types'; +import type { + PreferredSignInStrategy, + SignInFactor, + SignInFirstFactor, + SignInSecondFactor, + SignInStrategy, +} from '@clerk/types'; + +// Factor sorting - https://github.com/clerk/javascript/blob/5764e2911790051589bb5c4f3b1a2c79f7f30c7e/packages/clerk-js/src/ui/utils/factorSorting.ts +const makeSortingOrderMap = (arr: T[]): Record => + arr.reduce( + (acc, k, i) => { + acc[k] = i; + return acc; + }, + {} as Record, + ); + +const STRATEGY_SORT_ORDER_PASSWORD_PREF = makeSortingOrderMap([ + 'passkey', + 'password', + 'email_link', + 'email_code', + 'phone_code', +] as SignInStrategy[]); + +const STRATEGY_SORT_ORDER_OTP_PREF = makeSortingOrderMap([ + 'email_code', + 'email_link', + 'phone_code', + 'passkey', + 'password', +] as SignInStrategy[]); + +const makeSortingFunction = + (sortingMap: Record) => + (a: SignInFactor, b: SignInFactor): number => { + const orderA = sortingMap[a.strategy]; + const orderB = sortingMap[b.strategy]; + if (orderA === undefined || orderB === undefined) { + return 0; + } + return orderA - orderB; + }; -const ORDER_WHEN_PASSWORD_PREFERRED = ['passkey', 'password', 'email_link', 'email_code', 'phone_code'] as const; -const ORDER_WHEN_OTP_PREFERRED = ['email_link', 'email_code', 'phone_code', 'passkey', 'password'] as const; -// const ORDER_ALL_STRATEGIES = ['email_link', 'email_code', 'phone_code', 'password'] as const; +const passwordPrefFactorComparator = makeSortingFunction(STRATEGY_SORT_ORDER_PASSWORD_PREF); +const otpPrefFactorComparator = makeSortingFunction(STRATEGY_SORT_ORDER_OTP_PREF); const findFactorForIdentifier = (i: string | null) => (f: SignInFactor) => { return 'safeIdentifier' in f && f.safeIdentifier === i; @@ -43,27 +85,11 @@ function determineStrategyWhenPasswordIsPreferred(factors: SignInFirstFactor[], if (passkeyFactor) { return passkeyFactor; } - - // Prefer the password factor if it's available - const passwordFactor = factors.find(factor => factor.strategy === 'password'); - if (passwordFactor) { - return passwordFactor; - } - - // Otherwise, find the factor for the provided identifier, or the next factor based on the preference list - const factorForIdentifier = factors.find(findFactorForIdentifier(identifier)); - if (factorForIdentifier) { - return factorForIdentifier; - } - - for (const preferredFactor of ORDER_WHEN_PASSWORD_PREFERRED) { - const factor = factors.find(factor => factor.strategy === preferredFactor); - if (factor) { - return factor; - } + const selected = factors.sort(passwordPrefFactorComparator)[0]; + if (selected.strategy === 'password') { + return selected; } - - return null; + return factors.find(findFactorForIdentifier(identifier)) || selected || null; } function determineStrategyWhenOTPIsPreferred(factors: SignInFirstFactor[], identifier: string | null) { @@ -72,25 +98,16 @@ function determineStrategyWhenOTPIsPreferred(factors: SignInFirstFactor[], ident return passkeyFactor; } - const factorForIdentifier = factors.find(findFactorForIdentifier(identifier)); - if (factorForIdentifier) { - return factorForIdentifier; + const sortedBasedOnPrefFactor = factors.sort(otpPrefFactorComparator); + const forIdentifier = sortedBasedOnPrefFactor.find(findFactorForIdentifier(identifier)); + if (forIdentifier) { + return forIdentifier; } - - // Prefer the password factor if it's available - const emailLinkFactor = factors.find(factor => factor.strategy === 'email_link'); - if (emailLinkFactor) { - return emailLinkFactor; + const firstBasedOnPref = sortedBasedOnPrefFactor[0]; + if (firstBasedOnPref.strategy === 'email_link') { + return firstBasedOnPref; } - - for (const preferredFactor of ORDER_WHEN_OTP_PREFERRED) { - const factor = factors.find(factor => factor.strategy === preferredFactor); - if (factor) { - return factor; - } - } - - return null; + return factors.find(findFactorForIdentifier(identifier)) || firstBasedOnPref || null; } // The priority of second factors is: TOTP -> Phone code -> any other factor