Repository navigation
refactor(api): route both auth API layers through one implementation per operation - #1034
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. 5 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 8 reviews per hour. 📝 WalkthroughWalkthroughThe change centralizes sign-in and sign-up creation and redirect operations in shared ChangesAuthentication flow consolidation
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Refactor Merge Risk: ⚪ Minimal · up to No actionable issue is established for this change; the sign-up update payload remains an unverified compatibility question. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
1d76d45 to
1c07b53
Compare
…per operation
Clerk.auth methods now build the same typed params the SignIn/SignUp entry
points take (SignIn.CreateParams.Strategy, AuthenticateWithRedirectParams,
SignUp.CreateParams) and call a shared internal implementation in AuthFlows.kt.
SignIn.create, SignUp.create and both authenticateWithRedirect methods call the
same implementations, so request shape, locale and error reporting live in one
place.
Step verbs delegate to the general entry point: verifyCode, verifyWithPassword
and verifyWithPasskey go to attemptFirstFactor; verifyMfaCode to
attemptSecondFactor; reload to get; SignUp.verifyCode to attemptVerification;
SignUp.update {} to update(SignUpUpdateParams).
Two behaviour fixes fall out of the delegation:
- prepareSecondFactor(emailAddressId = ...) prepared the phone factor whenever
one existed. An explicit id now picks its channel.
- sendMfaPhoneCode / sendMfaEmailCode skipped the status check that
prepareSecondFactor makes. They now delegate to it.
…quests sendMfaPhoneCode and sendMfaEmailCode resolved the factor id before the status check, so on a sign-in still in needs_first_factor they threw IllegalStateException instead of returning sign_in_status_invalid. They, and the no-arg prepareSecondFactor, now check status first and return a second_factor_strategy_not_supported failure when no factor is available. Document prepareSecondFactor's id precedence, assert AuthLayerParityTest requests against literal maps now that both layers share one implementation, and rename the Map overload of Auth.createSignUp to postSignUp so it is not mistaken for the locale-free SignUp.create(Map).
1c07b53 to
759e61d
Compare
* refactor(ui): move the prebuilt UI onto the Clerk.auth facade Swap every old-layer auth call in source/ui for its Clerk.auth / step-verb equivalent from the #1034 matrix. Requests are unchanged. First-factor code attempts keep attemptFirstFactor because signIn.verifyCode infers the strategy instead of taking the one the UI chose. * test(ui): pin the MFA type and sign-up code channel the prebuilt UI sends SignInAttemptHandlerTest verified verifyMfaCode(any(), any()), so a handler sending the wrong MfaType still passed. It now verifies the exact code and type. SignUpCodeViewModel's email/phone mapping had no test at all.
Why
Architecture review finding #9: "Two public auth APIs that drift apart." About 23 operations have two to seven public entry points each, and each entry point built its own request. #1026 fixed the drift bugs the review found. This PR removes the cause: for each operation, every entry point now calls one implementation.
Stack: #1026 (drift fixes) → this PR → #1035 (facade gaps for the prebuilt UI).
Scope
auth/AuthFlows.ktholds the canonical implementations:createSignIn,authenticateSignInWithRedirect,createSignUpandauthenticateSignUpWithRedirect.Clerk.authmethod now builds the same typed params that the older entry point accepts, then calls one of these. The typed params areSignIn.CreateParams.Strategy,*.AuthenticateWithRedirectParamsandSignUp.CreateParams.SignIn.create,SignUp.createand bothauthenticateWithRedirectmethods call the same functions. Request shape,localeandAuthEvent.Errorreporting now live in one place.verifyCode,verifyWithPasswordandverifyWithPasskey→attemptFirstFactorverifyMfaCode→attemptSecondFactorreload→getSignUp.verifyCode→attemptVerificationSignUp.update {}→update(SignUpUpdateParams)sendMfaPhoneCode/sendMfaEmailCodeandprepareSecondFactor(ids)→prepareSecondFactor(strategy)prepareSecondFactor(emailAddressId = …)prepared the phone factor whenever the sign-in also supported phone. The prebuilt UI hits this when a user with both phone and email MFA picks email. An explicit id now picks its channel.sendMfaPhoneCode/sendMfaEmailCodeskipped theneeds_second_factor/needs_client_truststatus check thatprepareSecondFactormakes.ReplaceWithfollows once the UI migration PR lands, and removal in the next major.Operation × entry-point matrix
Legend:
AuthLayerParityTestpins rows 1–4, 16, 18, 22, 23, 27 and 29 against literal request maps, and rows 11, 12, 25 and 26 against theSSOServicecall. The other rows were checked by reading the code.Clerk.auth.signIn { email / phone / username / identifier }SignIn.create(Strategy.Identifier)→createSignIn≡;SignIn.create(Map)raw (no locale added)identifier =added in #1035Clerk.auth.signInWithPassword {}SignIn.create(Strategy.Password)→createSignIn≡;SignIn.create(Identifier(strategy = "password", password = …))→createSignInClerk.auth.signInWithOtp { email / phone }SignIn.create(Strategy.EmailCode / PhoneCode)→createSignIn≡Clerk.auth.signInWithTicketSignIn.create(Strategy.Ticket)→createSignIn≡Clerk.auth.signInWithPasskey(prefer…)SignIn.create(Strategy.Passkey)→ facade;SignIn.create(Passkey, prefer…)→ facade (#1035);SignIn.authenticateWithGoogleCredential([PASSKEY])same service callauthenticateWithGoogleCredentialstays the general multi-credential entry pointsignIn.authenticateWithPasskey()prepareSecondFactor(Passkey)+attemptSecondFactor(Passkey)are its stepsClerk.auth.signInWithBiometricsSignIn.create(Strategy.BiometricCredential)→createSignIn≡Clerk.auth.signInWithRestoreCredentialClerk.auth.signInWithGoogleOneTap(transferable)(#1035)SignIn.authenticateWithGoogleOneTap→ facade (#1035);Clerk.auth.signInWithIdToken { GOOGLE }takes your own tokenSignUp.authenticateWithGoogleOneTap()Clerk.auth.signUpWithGoogleOneTap()→signInWithGoogleOneTap(true): sign-in first, deliberately (#665)Clerk.auth.signInWithOAuth(provider, transferable, redirectUrl)SignIn.authenticateWithRedirect(OAuth)→authenticateSignInWithRedirect≡transferable/redirectUrladded in #1035. The old params also carryidentifier,emailAddressandlegalAccepted.Clerk.auth.signInWithEnterpriseSso { email }SignIn.authenticateWithRedirect(EnterpriseSSO)→authenticateSignInWithRedirect≡signIn.authenticateWithOAuth(…)/signIn.authenticateWithEnterpriseSso(…)(#1035)prepareFirstFactor(OAuth / EnterpriseSSO)+authenticateWithPreparedRedirectare its stepssignIn.sendEmailCode(id)/sendPhoneCode(id)signIn.sendCode { email / phone }resolves the factor, then →prepareFirstFactor(#1026);prepareFirstFactor(EmailCode / PhoneCode)signIn.sendEmailLink(id)prepareFirstFactor(EmailLink)needs a caller-made PKCE pair;Clerk.auth.startEmailLinkSignIn(email)creates and sendssignIn.verifyCode(code)/verifyWithPassword/verifyWithPasskeyattemptFirstFactor≡verifyCodeinfers the strategy fromfirstFactorVerificationsignIn.sendMfaPhoneCode(id)/sendMfaEmailCode(id)prepareSecondFactor(strategy);prepareSecondFactor(phoneNumberId, emailAddressId)→ samesignIn.verifyMfaCode(code, type)attemptSecondFactor≡signIn.sendResetPasswordEmailCode(id)/PhoneCode(id)(#1035)sendResetPasswordCode { email / phone }resolves the factor, then → those;prepareFirstFactor(ResetPassword*);SignIn.create(Strategy.ResetPassword*)→createSignInsignIn.resetPassword(…)signIn.reload()signIn.get()(reload → get ≡)Clerk.auth.signUp {}SignUp.create(Standard)→createSignUp≡;SignUp.create(Map)rawClerk.auth.signUpWithTicketSignUp.create(Ticket)→createSignUp≡Clerk.auth.signUpWithIdToken(token, GOOGLE) {}SignUp.create(GoogleOneTap(token))→createSignUp(no name fields)Clerk.auth.signUpWithOAuth(provider, redirectUrl, unsafeMetadata)SignUp.authenticateWithRedirect(OAuth)→authenticateSignUpWithRedirect≡redirectUrl/unsafeMetadataadded in #1035Clerk.auth.signUpWithEnterpriseSso { email }SignUp.authenticateWithRedirect(EnterpriseSSO)→authenticateSignUpWithRedirect≡signUp.update {}update(SignUpUpdateParams)≡SignUpBuilder.unsafeMetadatais still ignored here becauseSignUpUpdateParamshas no field for it. Follow-up.signUp.sendEmailCode()/sendPhoneCode()/sendEmailLink()signUp.sendCode {}→ those;prepareVerification(…)signUp.verifyCode(code, type)attemptVerification≡Clerk.auth.signOut/setActive/getToken/revokeSessionsession.fetchToken,session.revoke()getTokenandrevokeSessiondo not emitAuthEvent.Error. Token fetches are frequent, and their failures are not auth-flow errors.Clerk.auth.handle(uri),startHostedAuth,startEmailLinkSignIn/handleMagicLinkDeepLink/completeMagicLinkClerk.auth.nativeMagicLink.*NativeMagicLinkError, notClerkErrorResponsePrebuilt UI call sites → facade equivalent
Every old-layer call in
source/uinow has a facade equivalent (after #1035). The UI migration PR will switch these over once the UI ViewModel refactor (item 10) lands:AuthStartViewModel:SignIn.create(Passkey, preferImmediatelyAvailableCredentials = true)Clerk.auth.signInWithPasskey(preferImmediatelyAvailableCredentials = true)(#1035)AuthStartViewModel:SignIn.create(Identifier(identifier))Clerk.auth.signIn { identifier = … }(#1035)AuthStartViewModel:SignUp.create(Standard(email/phone/username, unsafeMetadata))Clerk.auth.signUp { … }AuthStartViewModel:SignIn.authenticateWithGoogleOneTap(transferable)Clerk.auth.signInWithGoogleOneTap(transferable)(#1035)AuthStartViewModel:SignIn.authenticateWithRedirect(OAuth(provider), transferable)Clerk.auth.signInWithOAuth(provider, transferable)(#1035)AuthStartViewModel:SignUp.authenticateWithRedirect(OAuth(provider, unsafeMetadata))Clerk.auth.signUpWithOAuth(provider, unsafeMetadata = …)(#1035)AuthStartViewModel:prepareFirstFactor(EnterpriseSSO())+authenticateWithPreparedRedirectsignIn.authenticateWithEnterpriseSso(transferable)(#1035)SignInRedirect:prepareFirstFactor(OAuth)+authenticateWithPreparedRedirectsignIn.authenticateWithOAuth(provider, transferable)(#1035)SignInPrepareHandler:prepareFirstFactor(EmailCode / PhoneCode(id))signIn.sendEmailCode(id)/sendPhoneCode(id)SignInPrepareHandler:prepareFirstFactor(ResetPasswordEmailCode / PhoneCode(id))signIn.sendResetPasswordEmailCode(id)/PhoneCode(id)(#1035)SignInPrepareHandler:prepareSecondFactor(phoneNumberId)/(emailAddressId = …)signIn.sendMfaPhoneCode(id)/sendMfaEmailCode(id)SignInAttemptHandler,SetPasswordViewModel:attemptFirstFactor(EmailCode / PhoneCode / ResetPassword* / Password)signIn.verifyCode(code)/verifyWithPassword(pw)SignInAttemptHandler,BackupCodeViewModel:attemptSecondFactor(TOTP / PhoneCode / EmailCode / BackupCode)signIn.verifyMfaCode(code, MfaType.*)PasskeyViewModel:SignIn.authenticateWithGoogleCredential([PASSKEY])Clerk.auth.signInWithPasskey()PasskeyViewModel:signIn.authenticateWithPasskey()ResetPasswordViewModel:signIn.resetPassword(…)SignUpCodeViewModel:prepareVerification(EmailCode / PhoneCode),attemptVerification(…)signUp.sendEmailCode()/sendPhoneCode(),signUp.verifyCode(code, type)SignUpEmailLinkViewModel:prepareVerification(EmailLink())signUp.sendEmailLink()CollectFieldViewModel,CompleteProfileViewModel:update(SignUpUpdateParams.Standard)signUp.update { … }Blast Radius
localehandling is unchanged too:SignIn.create(Map)/SignUp.create(Map)still pass the map through as given.SignIn.canPrepareSecondFactor/invalidSecondFactorStateare newinternalhelpers.Strategysealed class, feat(api): add sealed OAuthResult.Outcome and drop UI force-unwraps #1030OAuthResult.outcome). The canonical functions take the existing typed params and returnOAuthResult, so they can switch toStrategyin one place without depending on either PR. TurningOAuthResultitself into a sealed type is deferred to item 9's follow-up, the UI migration and deprecation PR.Auth.removeSessionLocally, which this PR does not touch.internaldeclarations are unaffected. If build: enable explicit API mode for api, ui and telemetry #1023 lands first, no new public declarations are added in this PR.Testing
AuthLayerParityTest(new) pins that each pair of entry points sends Clerk the same, literal request. These pass before and after: they guard against future drift.SignInPrepareSecondFactorTest(new, 4 cases). Two cases failed against fix(api): stop the two auth API layers drifting on enterprise sign-up, sendCode and error events #1026's sources (stashed and run): the email-channel fix and thesendMfaPhoneCodestatus check../gradlew :source:api:spotlessApply :source:api:detekt :source:api:testDebugUnitTest: all pass../gradlew :source:ui:testDebugUnitTest: all pass. The prebuilt UI calls the old layer and is unchanged.Review follow-ups
The "fail second-factor sends without throwing and pin parity requests" commit addresses review feedback:
sendMfaPhoneCode/sendMfaEmailCodechecked status only after resolving the factor id, so on aneeds_first_factorsign-in (wheresupported_second_factorsis null) they threwIllegalStateException. They now check status first and, like the no-argprepareSecondFactor(), return asecond_factor_strategy_not_supportedfailure instead of throwing when no factor is available.prepareSecondFactor(phoneNumberId, emailAddressId)KDoc documents the precedence: explicit phone, explicit email, then supported factors.AuthLayerParityTestasserts literal request maps. Both layers share one implementation, so comparing them with each other alone could no longer catch drift.Mapoverload ofAuth.createSignUpis renamedpostSignUp(mirroringpostSignIn), so it isn't mistaken for the locale-freeSignUp.create(Map).🤖 Generated with Claude Code
Summary by CodeRabbit