auth_grace 0.0.6
auth_grace: ^0.0.6 copied to clipboard
Smart biometric auth with grace period. Skips prompt if phone was recently unlocked.
auth_grace #
Smart biometric authentication with an automatic grace period — skips the prompt if the phone was recently unlocked, exactly like Google Pay.
local_auth only shows a biometric prompt. auth_grace adds the missing
layer: if the device was unlocked within the last N seconds, authentication
is granted silently without interrupting the user.
Grace period in action — no prompt while the device is still warm.
Platform support #
| Android | iOS |
|---|---|
| ✅ API 23+ | ✅ iOS 12+ |
Android — grace period is enforced at the hardware level via an Android Keystore time-bound AES key (Trusted Execution Environment).
iOS — grace period is approximated via a Keychain timestamp recorded after
every successful local_auth prompt.
Features #
- ⚡ Zero-friction re-auth — skips the prompt while the device is still "warm" (recently unlocked)
- 🔒 Strict mode —
alwaysRequire: truebypasses the grace period for payment confirmation flows - 🔑 Hardware-backed — Android Keystore TEE, iOS Secure Enclave
- 🛡️ Key invalidation handling — silently regenerates the key when the user changes enrolled biometrics
- 📱 Emulator-safe — automatically falls back to
local_authon emulators (no real TEE) - 🔄 Lifecycle-aware — re-authenticate on app resume via
WidgetsBindingObserver
Installation #
Add to your pubspec.yaml:
dependencies:
auth_grace: ^0.0.6
Then run:
flutter pub get
Android setup #
1. Permissions #
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
2. MainActivity #
MainActivity must extend FlutterFragmentActivity (required by
local_auth for the biometric dialog):
// android/app/src/main/kotlin/…/MainActivity.kt
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()
iOS setup #
Add the NSFaceIDUsageDescription key to ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Used to authenticate you when opening the app.</string>
No other setup is required.
Usage #
Basic — 30-second grace window #
import 'package:auth_grace/auth_grace.dart';
final auth = AuthGrace(
options: const AuthGraceOptions(
gracePeriodSeconds: 30,
reason: 'Authenticate to open',
),
);
// Call once at app startup
await auth.init();
// Call whenever identity must be verified
final result = await auth.authenticate();
if (result.isSuccess) {
// AuthStatus.success → biometric / PIN passed
// AuthStatus.gracePeriodActive → device was recently unlocked, prompt skipped
navigateToHome();
}
Strict mode — always prompt (payments) #
final paymentAuth = AuthGrace(
options: const AuthGraceOptions(
alwaysRequire: true,
reason: 'Confirm payment',
),
);
final result = await paymentAuth.authenticate();
Re-authenticate on app resume #
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_authenticate();
}
}
Future<void> _authenticate() async {
final result = await auth.authenticate();
// handle result
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Logout / account switch #
// Clears Keystore key (Android) and Keychain timestamp (iOS)
await auth.reset();
API reference #
AuthGrace #
| Method | Returns | Description |
|---|---|---|
AuthGrace({AuthGraceOptions? options}) |
— | Constructor. Defaults to a 30-second grace window. |
init() |
Future<void> |
Generate the Keystore/Keychain key. Call once at startup. |
authenticate() |
Future<AuthResult> |
Authenticate — skips prompt if within grace period. |
isWithinGracePeriod() |
Future<bool> |
Check grace status without showing a prompt. |
isAvailable() |
Future<bool> |
true if the device has enrolled biometrics or a PIN. |
isHardwareBacked() |
Future<bool> |
true if the device has a hardware secure element. |
reset() |
Future<void> |
Delete key/timestamp — call on logout. |
AuthGraceOptions #
| Parameter | Type | Default | Description |
|---|---|---|---|
gracePeriodSeconds |
int |
30 |
Seconds after unlock before re-prompting. |
alwaysRequire |
bool |
false |
Always prompt regardless of grace period. |
reason |
String |
'Authenticate to continue' |
Biometric dialog message. |
allowDeviceCredential |
bool |
true |
Allow PIN/pattern as fallback. |
persistAcrossBackgrounding |
bool |
true |
Keep the biometric prompt visible when the app is backgrounded mid-auth. |
keyName |
String |
'com.authgrace.auth_grace_key' |
Custom Keystore key name. |
AuthResult #
| Property | Type | Description |
|---|---|---|
status |
AuthStatus |
High-level outcome (see below). |
method |
AuthMethod |
How authentication was performed. |
error |
String? |
Error message when status == AuthStatus.error. |
isSuccess |
bool |
true for success and gracePeriodActive. |
AuthStatus enum #
| Value | Meaning |
|---|---|
success |
Biometric or PIN passed. |
gracePeriodActive |
Device was recently unlocked — prompt skipped. |
failed |
User cancelled or failed too many times. |
notAvailable |
No enrolled biometrics on this device. |
error |
Unexpected platform error — inspect AuthResult.error. |
AuthMethod enum #
| Value | Meaning |
|---|---|
biometric |
Face ID, Touch ID, or fingerprint sensor was used. |
deviceCredential |
PIN, pattern, or password was used as fallback. |
gracePeriod |
Prompt was skipped — device was recently unlocked. |
none |
No authentication took place. |
Edge cases #
| Scenario | Behaviour |
|---|---|
| Emulator | isEmulator() detected — Keystore check skipped, falls through to local_auth. |
| Biometric enrolled/removed | KeyPermanentlyInvalidatedException caught — key deleted and regenerated silently. |
| No biometric hardware | isAvailable() returns false → AuthStatus.notAvailable. |
gracePeriodSeconds = 0 |
Always requires authentication — no grace. |
alwaysRequire = true |
Grace period check skipped entirely. |
| App backgrounded and resumed | Call authenticate() in didChangeAppLifecycleState. |
| Logout / user switch | Call auth.reset() to delete key and Keychain timestamp. |
Platform notes #
Android — Grace period is enforced natively by the Android Keystore hardware (TEE). The OS manages the auth window — your app code cannot bypass it.
iOS — Grace period is simulated via a Keychain timestamp recorded after each successful authentication. Functionally identical for most use cases, but software-managed rather than hardware-enforced.
Security model #
auth_grace is a UX friction layer, not a cryptographic access control
system. Understanding its boundaries helps you use it correctly.
What it protects against
- A stranger picking up an unlocked phone after the grace period expires — the next open will require biometrics.
- Passive shoulder-surfing — the biometric prompt adds meaningful friction for casual observers.
- Unauthorized access after the device is re-locked.
What it does NOT protect against
- A coerced user (someone forced to authenticate) — no biometric library can prevent this.
- Rooted (Android) or jailbroken (iOS) devices — the Keystore / Keychain integrity guarantees do not hold on compromised systems.
- Access during the grace window itself — if someone grabs the phone in those 30 seconds, they get in. That is the intended trade-off.
- iOS Keychain timestamp manipulation — the iOS grace period is software-managed. A sophisticated attacker with direct Keychain access could theoretically alter the timestamp. Android's TEE-enforced key is not vulnerable to this.
Appropriate use cases
Confirming the active user's identity before showing sensitive content (account balance, health data) or initiating a local action (like Google Pay's tap-to-pay). This mirrors how the OS itself uses biometrics.
Not appropriate on its own for
Authorising server-side transactions or protecting encryption keys. Always
pair auth_grace with server-side verification for any action with real
financial or security consequences.
Known limitations #
- Grace period timing on Android may vary by ±1–2 seconds (OS-level).
- iOS grace period is Keychain-timestamp-based, not hardware-enforced.
- Emulators always skip the grace period check (by design — no real TEE).
- Devices below Android 6.0 (API 23) are not supported.
alwaysRequire: trueon iOS still has a briefLAContextsession (~30 s) — this is iOS system behaviour and cannot be overridden.