Skip to content

Support biometric authentication with advanced authentication - #4136

Merged
brandonpage merged 4 commits into
forcedotcom:devfrom
brandonpage:biometric-with-advanced-auth
Aug 21, 2026
Merged

Support biometric authentication with advanced authentication#4136
brandonpage merged 4 commits into
forcedotcom:devfrom
brandonpage:biometric-with-advanced-auth

Conversation

@brandonpage

@brandonpage brandonpage commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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.swift
    • Added suppressInitialBrowserAuthentication, a one-shot flag armed by lock() so the browser attempt it triggers doesn't race the biometric prompt. Consumed (cleared) by the gate on its next check.
    • On biometric cancel/failure, wait for the scene to return to .foregroundActive (dismissing the system biometric sheet briefly leaves it inactive, and ASWebAuthenticationSession.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.
    • Guard 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.
    • Added 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 to SFLoginViewController's existing biometric button), shown only when biometric is locked, opted-in, and available. Reuses the existing biometricLoginButton localized string — no new strings.

Tests

  • New BiometricAdvancedAuthGateTests.swift covering the gate / suppression-flag behavior.
  • Extended SFUserAccountManagerTests.m and SFSDKLoginHostTests.m.
  • SalesforceSDKCore builds clean; new + affected suites pass on the simulator. Also device-tested on a physical iPhone for the Face ID cancel → browser flow.

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.
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
1 Warning
⚠️ Static Analysis found an issue with one or more files you modified. Please fix the issue(s).

Clang Static Analysis Issues

File Type Category Description Line Col
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1675 15
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1690 15
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 1st parameter 2366 21
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 2nd parameter 2584 13

Generated by 🚫 Danger

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
TestsPassed ✅SkippedFailed
SalesforceSDKCore iOS ^18 Test Results996 ran996 ✅
TestResult
No test annotations available

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.23529% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.28%. Comparing base (26f7999) to head (47c3da6).
⚠️ Report is 14 commits behind head on dev.

Files with missing lines Patch % Lines
...ation/BiometricAuthenticationManagerInternal.swift 80.24% 16 Missing ⚠️
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     
Components Coverage Δ
Analytics 70.78% <ø> (ø)
Common 70.88% <ø> (ø)
Core 60.84% <89.04%> (-6.32%) ⬇️
SmartStore 73.44% <ø> (ø)
MobileSync 88.82% <ø> (ø)
Files with missing lines Coverage Δ
...Login/LoginHost/SFSDKLoginHostListViewController.m 70.14% <100.00%> (-14.88%) ⬇️
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 49.70% <100.00%> (-17.18%) ⬇️
...ation/BiometricAuthenticationManagerInternal.swift 79.39% <80.24%> (+10.38%) ⬆️

... and 34 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

Copy link
Copy Markdown
TestsPassedSkippedFailed ❌️
AuthFlowTester UI Test Results all1 ran1 ❌
TestResult
AuthFlowTester UI Test Results all
AuthFlowTesterUITests.xctest
LegacyLoginTests.testCAOpaque_DefaultScopes_WebServerFlow()❌ failure

@wmathurin wmathurin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@github-actions

Copy link
Copy Markdown
1 Warning
⚠️ Big PR, try to keep changes smaller if you can.

Generated by 🚫 Danger

@brandonpage
brandonpage merged commit 1c2dd30 into forcedotcom:dev Aug 21, 2026
23 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants