Support biometric authentication with advanced authentication - #4136
Merged
brandonpage merged 4 commits intoAug 21, 2026
Conversation
Add advanced-auth (browser) fallback support to the biometric lock flow so a biometric-locked user who declines or fails the biometric prompt can still complete login via the browser.
Clang Static Analysis Issues
Generated by 🚫 Danger |
|
||||||||||||||
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #4136 +/- ##
==========================================
- Coverage 71.50% 67.28% -4.22%
==========================================
Files 254 254
Lines 22670 22785 +115
==========================================
- Hits 16211 15332 -879
- Misses 6459 7453 +994
🚀 New features to boost your workflow:
|
|
||||||||||||||||||
wmathurin
reviewed
Aug 17, 2026
wmathurin
left a comment
Contributor
There was a problem hiding this comment.
Inline review comment.
Refactor for testability (no behavior change): extract handleBiometricCancellation(_:) from presentBiometric's catch block, split awaitSceneActivation(_:) out of waitForSceneActive(_:), and widen these plus ResumeGuard to internal so the async cancellation path (unreachable in CI because LAContext.evaluatePolicy can't run in the simulator) can be exercised directly. Add 7 tests covering the suppressed-browser resume, the cancellation handler, scene-activation await, and the one-shot resume latch. All new SFUserAccountManager.m and BiometricAuthenticationManagerInternal.swift methods now reach 100% line coverage on their reachable paths.
lock() arms browser suppression based on showNativeLoginButton()'s capability check, but presentBiometric(scene:) checked a separately-built LAContext. If biometric became unavailable between the two (hardware busy, enrollment removed), the browser stayed suppressed while no prompt appeared, stranding the user. Both paths now consult a shared biometricAvailable() helper. When biometric is unavailable at prompt time, presentBiometric disarms suppression so the browser-auth gate lets Advanced Auth / username-password proceed normally. Adds tests for biometricAvailable() and the unavailable-at-prompt disarm path.
login() fans out to every connected scene, and each reaches willBeginBrowserAuthentication: independently. The single global suppressInitialBrowserAuthentication one-shot was consumed by the first scene, letting later scenes launch ASWebAuthenticationSession while still biometrically locked. Replace it with a per-scene suppression set keyed by scene persistentIdentifier: lock() arms each connected scene, the gate consumes only that scene's flag, and presentBiometric disarms only its own scene on the capability race. Adds a two-scene gate regression test plus a per-scene consume invariant test.
Generated by 🚫 Danger |
wmathurin
approved these changes
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Suppress Advanced Authentication (
ASWebAuthenticationSession) to show OS Biometric Prompt if locked. When a biometric-locked user declines or fails the Face ID / Touch ID prompt (e.g. taps "Use Password" or cancels), the login screen is automatically launched.What changed
BiometricAuthenticationManagerInternal.swiftsuppressInitialBrowserAuthentication, a one-shot flag armed bylock()so the browser attempt it triggers doesn't race the biometric prompt. Consumed (cleared) by the gate on its next check..foregroundActive(dismissing the system biometric sheet briefly leaves it inactive, andASWebAuthenticationSession.start()silently no-ops from a non-active scene), then resume the suppressed browser session in place — keeping its covering window up so the app is never exposed.handleAppForeground()against re-locking (and re-arming the flag) while already locked.SFUserAccountManager(.h/.m)willBeginBrowserAuthentication:gate honors the suppression flag: suppresses the browser and presents the login-host picker as a fallback landing screen when biometric is locked; otherwise proceeds normally.resumeBrowserAuthentication:to resume the suppressed session's held browser-launch callback (falls back to a fresh login if the callback is gone).isAlreadyPresentingLoginController:now also recognizes the host-list picker so the Advanced Auth retry dismisses it before presenting the browser.SFSDKLoginHostListViewController.m— adds a "retry biometric" nav-bar button (picker-screen counterpart toSFLoginViewController's existing biometric button), shown only when biometric is locked, opted-in, and available. Reuses the existingbiometricLoginButtonlocalized string — no new strings.Tests
BiometricAdvancedAuthGateTests.swiftcovering the gate / suppression-flag behavior.SFUserAccountManagerTests.mandSFSDKLoginHostTests.m.SalesforceSDKCorebuilds clean; new + affected suites pass on the simulator. Also device-tested on a physical iPhone for the Face ID cancel → browser flow.