biometric_auth_login
A lightweight, backend-agnostic biometric unlock layer for Flutter. It gates a single opaque secret behind the device's biometrics (Face ID / Touch ID / fingerprint / Windows Hello) and stores it in platform-encrypted secure storage β with a ready-made unlock button included.
The package never interprets the secret. Your app decides whether it's a refresh token, a session id, or JSON credentials, so the package stays completely independent of any backend, API, or state-management choice.
Biometric authentication here is a local, device-level gate. It proves the person holding the device can pass the device's own biometric check β it does not prove identity to your backend. See Security.
What you get
- π Biometric availability & kind detection (face / fingerprint / iris).
- π System biometric prompt with graceful, typed error handling.
- π Secure storage only β Android Keystore-backed cipher storage, iOS/macOS
Keychain, Windows DPAPI.
SharedPreferences, plain files, etc. are never used. - π§© Simple opaque-secret lifecycle:
saveSecret/unlock/deleteSecret. - π‘οΈ Overlapping-prompt protection (no double prompts) built in.
- π¨ Ready-made, themeable
BiometricLoginButtonwith auto icon/label. - β Null-safe, documented API, unit + widget tests, runnable example.
Why not just local_auth?
local_auth answers one question: "did the device's biometric check pass?" It
does not store anything. This package builds the common unlock flow on top of
it:
local_auth |
flutter_secure_storage |
biometric_auth_login | |
|---|---|---|---|
| Biometric prompt | β | β | β (wraps it) |
| Secure secret storage | β | β | β (wraps it) |
| Secret lifecycle (enable/unlock/logout) | β | β | β |
| Typed result + retry/settings hints | β | β | β |
| Overlapping-prompt guard | β | β | β |
| Ready-made unlock button | β | β | β |
| Backend-agnostic (opaque secret) | β | β | β |
If you only need the raw prompt, use local_auth directly. Use this package when
you want the whole "unlock my stored session with biometrics" flow.
When to use it
- Re-opening an already-authenticated session without retyping a password.
- Unlocking sensitive screens (wallets, banking/finance, enterprise apps).
- Gating access to a locally stored refresh token / session credential.
When not to use it
- As backend authentication or server-side identity verification β it isn't one.
- For biometric registration with a backend (e.g. WebAuthn/passkeys).
- For camera-based face recognition or custom biometric matching.
Installation
dependencies:
biometric_auth_login: ^1.1.0
import 'package:biometric_auth_login/biometric_auth_login.dart';
Platform setup
Android
-
local_authrequires aFragmentActivity. Inandroid/app/src/main/kotlin/.../MainActivity.kt:import io.flutter.embedding.android.FlutterFragmentActivity class MainActivity : FlutterFragmentActivity() -
Add biometric permission(s) to
AndroidManifest.xml:<uses-permission android:name="android.permission.USE_BIOMETRIC" />
iOS
Add a Face ID usage description to ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely sign in to your account.</string>
macOS
- Enable the Keychain Sharing capability (required by
flutter_secure_storage) in bothmacos/Runner/DebugProfile.entitlementsandmacos/Runner/Release.entitlements. - Add
NSFaceIDUsageDescriptiontomacos/Runner/Info.plistfor Touch ID. - Requires a signed app and a Mac with Touch ID (or a paired Apple Watch / password fallback).
Windows
- No extra setup for a default build. Biometrics use Windows Hello; secure storage uses DPAPI.
- β οΈ
biometricOnlyis not enforceable on Windows β Windows Hello may allow a PIN fallback. See Limitations.
Basic usage
final biometric = BiometricLogin(
config: const BiometricConfig(secretKey: 'my_app.session'),
);
// Can we run a biometric prompt right now?
if (await biometric.canAuthenticate()) {
// Store whatever your app wants protected (prefer a refresh/session token).
await biometric.saveSecret(myRefreshToken);
// Later, unlock it behind the biometric gate.
final result = await biometric.unlock();
if (result.isSuccess) {
final secret = result.secret!; // hand this to YOUR login logic
}
// On logout, clear it.
await biometric.deleteSecret();
}
Full lifecycle
// 1. After a normal (password) login, capture a durable credential and gate it.
await biometric.saveSecret(session.refreshToken); // runs the enable prompt
// 2. On next launch, if set up, offer biometric unlock.
if (await biometric.isEnabled()) {
final result = await biometric.unlock(); // runs the sign-in prompt
if (result.isSuccess) {
await api.refreshSession(result.secret!); // your backend call
}
}
// 3. On logout, delete the secret (never throws).
await biometric.deleteSecret();
Handling the result
unlock() returns a BiometricUnlockResult. Use the BiometricStatus
extension helpers to decide what to do next β without hard-coding every case:
final result = await biometric.unlock();
if (result.isSuccess) {
useSecret(result.secret!);
return;
}
final status = result.status;
if (status.requiresEnrollmentInApp) {
// notEnabled β nothing saved yet; run saveSecret first.
} else if (status.requiresDeviceSetup) {
// notEnrolled β hardware exists but no biometric is enrolled.
showMessage('Enable biometrics in Settings, then try again.');
} else if (status.shouldFallBack) {
// unavailable / lockedOut / storageError β biometrics can't return the
// secret now; fall back to password login (and re-save the secret if needed).
showPasswordLogin();
} else if (status.canRetry) {
// canceled / failed / busy β offer a retry.
showRetry();
}
BiometricStatus values: success, unavailable, notEnrolled, notEnabled,
canceled, lockedOut, failed, storageError, busy, unknown.
BiometricLoginButton
BiometricLoginButton(
biometric: biometric,
onUnlocked: (secret) async {
// App-specific login with the unlocked secret.
},
onError: (status) {
// Show a message for canceled / lockedOut / ...
},
)
The button auto-detects the biometric kind to pick its icon and label, shows a
loading indicator while the prompt is up, ignores taps while an unlock is already
running, and contains no login logic. Customize with BiometricButtonStyle, or
override label / icon / kind. For full manual control, pass onPressed
instead of biometric / onUnlocked.
Security
- Local gate, not backend auth. A biometric success only hands your secret back. Your app remains responsible for using it to authenticate against your backend. A biometric pass does not prove identity to a server.
- Store a revocable credential, not a raw password. Prefer a refresh token or session id you can revoke server-side over a user's plaintext password.
- Secrets never touch insecure storage. They live only in platform-encrypted storage (Android Keystore / iOSΒ·macOS Keychain / Windows DPAPI).
- No logging of secrets. The package never logs secret values, and
BiometricUnlockResult.toString()masks the secret. Don't logresult.secretyourself. - Device-bound. iOS/macOS Keychain items use
first_unlock_this_deviceaccessibility and Android keys are Keystore-bound, so a restored backup on a new device can't reuse the secret β the user re-enrolls there. - The device enrollment is the trust anchor. Anyone whose biometric is enrolled on the device can pass the gate. This is inherent to the platform, not specific to this package.
- Keep secrets in memory only as long as needed after unlocking.
We do not claim any guarantee the underlying platform APIs don't provide.
Limitations
- No backend logic. The package gates access to your stored secret; wiring it to your auth flow is up to you.
- One secret per config. Each
BiometricLoginmanages one secret keyed byBiometricConfig.secretKey. Use different keys for multiple secrets. biometricOnlyis best-effort per platform. Enforced on Android/iOS; on Windows, Windows Hello may permit a PIN.- Default prompt strings are English. Pass localized
signInReason/enableReason(or per-callreason). - Enrollment changes. Adding/removing biometrics is an OS-level event; the
gate uses whatever is currently enrolled. If a platform invalidates the stored
secret,
unlock()surfacesstorageErrorand you can re-enable.
Platform support
| Platform | Biometrics | Secure storage | Notes |
|---|---|---|---|
| Android | β BiometricPrompt | β Keystore | Needs FragmentActivity + permission |
| iOS | β Face/Touch ID | β Keychain | Needs NSFaceIDUsageDescription |
| macOS | β Touch ID | β Keychain | Needs Keychain Sharing entitlement; signed app |
| Windows | β Windows Hello | β DPAPI | biometricOnly not enforceable |
| Web / Linux | β | β | Not supported |
Automated tests here are unit/widget tests with mocked platform channels.
Real biometric prompts, Keystore/Keychain/DPAPI behavior, and enrollment-change
handling must be verified manually on physical devices/emulators β no CI can
present a real fingerprint. See test/ for what is covered automatically.
FAQ
Is this a replacement for backend authentication? No. It's a local device gate over a secret you already obtained from your backend.
Should I store the user's password? Prefer a refresh/session token you can revoke server-side. If you must store credentials, treat them as highly sensitive.
What happens if the user removes their fingerprint / adds a new one?
The gate uses whatever is currently enrolled. If the platform invalidates the
stored secret, unlock() returns storageError; re-run saveSecret to re-enable.
What happens when biometrics are unavailable?
unlock() returns unavailable (no hardware / not set up) or notEnrolled
(hardware but nothing enrolled). Use status.shouldFallBack /
status.requiresDeviceSetup to branch.
Does it work offline? Yes β the biometric check and secure storage are entirely on-device. What you do with the unlocked secret (e.g. calling your API) may need connectivity.
Does it work on Windows / macOS?
Yes, with the setup above. Note biometricOnly isn't enforceable on Windows.
Example
See example/ for a runnable app demonstrating availability checks,
enabling a secret, unlocking, result-state handling, and logout.
License
MIT β see LICENSE.
Libraries
- biometric_auth_login
- Backend-agnostic biometric (Face ID / Touch ID / fingerprint) gate with secure secret storage and a ready-made login button for Flutter.