LocalAuthentication.authenticate called without biometricOnly enforcement

ID

dart.auth.weak_biometric_authentication

Severity

high

Remediation Complexity

medium

Remediation Risk

high

Remediation Effort

medium

Resource

Auth

Language

Dart

Description

LocalAuthentication.authenticate(localizedReason: …) is invoked without biometricOnly: true. The default AuthenticationOptions allows the device PIN / pattern / passcode as a fallback, which defeats biometric protection for high-value flows.

Rationale

package:local_auth is the standard biometric API for Flutter. By default it falls back to the device PIN / pattern / passcode when biometrics are unavailable, not enrolled, or rejected by the user. For low-stakes flows ("unlock the app to view balance") that fallback is acceptable — the same protections that secure the device’s lock screen apply.

For high-value, intent-confirming operations — releasing a payment, exporting recovery codes, deleting an account, authorising a passkey enrollment — the user’s deliberate biometric presentation is the signal of intent. Falling back to the device passcode means anyone who knows the lock-screen passcode (shoulder-surfing, an over-the-shoulder family member, a coerced user) can authorise the action. Passing biometricOnly: true removes the passcode fallback.

The rule narrows by the presence of localizedReason: (the signature shape of local_auth) so it doesn’t fire on unrelated .authenticate(…​) calls in other packages.

The following code illustrates a vulnerable pattern detected by this rule:

class WeakBiometric {
  Future<bool> auth1() async {
    final local = LocalAuthentication();
    // VULNERABLE: LocalAuthentication.authenticate called without biometricOnly enforcement
    return await local.authenticate(
      localizedReason: 'Please authenticate to continue',
    );
  }
}

Remediation

For operations that require positive biometric proof, set biometricOnly: true explicitly. Handle the "no biometrics enrolled" case at the application layer (offer to enroll, or refuse the high-value action), rather than silently falling back to a weaker authenticator.

// Vulnerable: allows passcode fallback for a payment
await localAuth.authenticate(
  localizedReason: 'Authorize this payment',
);

// Fixed: biometric proof required
final ok = await localAuth.authenticate(
  localizedReason: 'Authorize this payment',
  options: const AuthenticationOptions(
    biometricOnly: true,
    stickyAuth: true,
  ),
);
if (!ok) return;   // refuse the action

Configuration

This detector does not need any configuration.

References

© 2026 Xygeni. All rights reserved