BiometricShield
A composable Flutter SDK that wraps biometric authentication into an injectable, namespace-aware layer with typed results, fallback chains, and audit event emission.
BiometricShield is a bridge — it sits between your app's existing auth system (Firebase, Supabase, Amplify, custom JWT, whatever) and the device's biometric hardware. You keep your backend. BiometricShield handles the local unlock layer: session management, lockout logic, fallback chains, token storage, and audit events.
Why
Every Flutter app that adds Face ID or fingerprint ends up writing the same 500+ lines of scattered boilerplate. BiometricShield collapses it into one injectable instance with typed results and zero exceptions.
final shield = BiometricShield();
final result = await shield.authenticate(reason: 'Unlock your account');
result.when(
success: (session, token) => proceedWithToken(token),
tokenExpired: () => navigateToLogin(),
cancelled: () => showCancelledState(),
lockedOut: (until) => showLockoutScreen(until),
unavailable: (reason, _) => fallbackToFullLogin(),
invalidated: () => promptReenrollment(),
fallbackSuccess: (method, session, token) => proceedWithToken(token),
sessionValid: (session, token) => proceedWithToken(token),
reauthenticationRequired: (reason) => navigateToLogin(),
error: (message, cause) => showError(message),
);
No try/catch. No boolean checks. Every outcome is a named variant you pattern-match on.
Install
# pubspec.yaml
dependencies:
biometric_shield: ^0.1.0
Platform setup
iOS — Add to ios/Runner/Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>This app uses Face ID to securely authenticate your identity.</string>
Minimum deployment target: iOS 12.0.
Android — Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
Minimum SDK: 23. Recommended: 28+ for BiometricPrompt (Class 2/3 distinction).
Quick start
1. Create the instance
import 'package:biometric_shield/biometric_shield.dart';
// Works with zero config
final shield = BiometricShield();
// Or configure everything
final shield = BiometricShield(
config: BiometricConfig(
sessionDuration: const Duration(minutes: 15),
maxAttempts: 3,
lockoutDuration: const Duration(minutes: 5),
fallbackChain: const [BiometricFallback.deviceCredential],
onEvent: (event) => myAnalytics.track(event.type.name, event.properties),
),
);
Provide it to your app however you want — Provider, Riverpod, GetIt, or just a global. The SDK doesn't care.
2. Store a token after server login
// After your server auth succeeds (Firebase, Amplify, REST, etc.)
await shield.storeToken(serverJwt, userId: user.id);
3. Authenticate with biometrics
final result = await shield.authenticate(
reason: 'Unlock your account',
userId: user.id,
);
result.when(
success: (session, token) => proceedWithToken(token),
tokenExpired: () => navigateToLogin(),
// ... handle all 10 outcomes
);
4. Protect screens with widgets (optional Flutter UI)
import 'package:biometric_shield/biometric_shield_ui.dart';
// Simple gate — shows child only after auth
BiometricGate(
shield: shield,
reason: 'Confirm to view health records',
reauthOnResume: true,
child: HealthRecordsScreen(),
fallbackWidget: (result) => AccessDeniedScreen(),
)
// Full control — you handle every state
BiometricBuilder(
shield: shield,
reason: 'Confirm identity',
builder: (context, state) => switch (state) {
AuthIdle() || AuthAuthenticating() => LoadingIndicator(),
AuthAuthenticated(:final token) => SecureContent(token: token),
AuthFailed(:final result) => FailureScreen(result: result),
},
)
Architecture
┌─────────────────────────────────────────┐
│ Your App │
├─────────────────────────────────────────┤
│ UI Layer (optional Flutter) │
│ BiometricBuilder · BiometricGate │
│ MaterialFallbackHandler │
├─────────────────────────────────────────┤
│ Core Layer (pure Dart, no Flutter) │
│ BiometricShield · BiometricConfig │
│ BiometricResult (sealed, 10 variants) │
├─────────────────────────────────────────┤
│ Subsystems │
│ SessionManager · LockoutManager │
│ FallbackChain · CapabilityDetector │
├─────────────────────────────────────────┤
│ Integration Points (you implement) │
│ TokenLifecycle · PolicyProvider │
│ FallbackHandler · TokenStoreInterface │
└─────────────────────────────────────────┘
The core is pure Dart with zero Flutter imports. All UI lives in the optional biometric_shield_ui.dart barrel. The core is testable without WidgetsFlutterBinding.
Three imports, three purposes
import 'package:biometric_shield/biometric_shield.dart'; // Core (pure Dart)
import 'package:biometric_shield/biometric_shield_ui.dart'; // Flutter widgets
import 'package:biometric_shield/biometric_shield_testing.dart'; // Mocks & fakes
Design principles
- Inject, don't replace. Wraps the local unlock layer only. Never touches your server auth.
- Instance-based, not static. You control scope and lifetime via your DI strategy.
- Pure Dart core, optional Flutter UI. Core has zero widget imports.
- Typed results, no exceptions. Sealed result type with
.when()exhaustive matching. - Namespace-aware. Multiple users on one device never share sessions or tokens.
- Platform-honest. Capability detection is a rich object, not a boolean.
- Reactive by default. Session state via Streams for countdown timers and auto-lock.
- Inverted UI control. The SDK never shows its own UI — it delegates to
FallbackHandler.
Backend integration
BiometricShield is backend-agnostic by design. Wire up any backend through two optional interfaces:
TokenLifecycle — token refresh
class FirebaseTokenLifecycle implements TokenLifecycle {
@override
Future<TokenStatus> validate(String token) async {
final user = FirebaseAuth.instance.currentUser;
final result = await user?.getIdTokenResult();
if (result == null) return TokenStatus.missing;
if (result.expirationTime!.isBefore(DateTime.now())) {
return TokenStatus.expired;
}
return TokenStatus.valid;
}
@override
Future<TokenRefreshResult> refresh(String expiredToken) async {
final user = FirebaseAuth.instance.currentUser;
final newToken = await user?.getIdToken(true);
if (newToken == null) return TokenRefreshResult.reauthRequired();
return TokenRefreshResult.success(newToken: newToken);
}
}
PolicyProvider — server-driven rules
class AppPolicyProvider implements PolicyProvider {
final ApiClient api;
AppPolicyProvider(this.api);
@override
Future<BiometricPolicy> getPolicy({String? userId}) async {
try {
final response = await api.get('/auth/biometric-policy');
return BiometricPolicy(
maxSessionDuration: Duration(minutes: response['max_session_min']),
disabled: response['biometric_disabled'],
disabledReason: response['disabled_reason'],
);
} catch (_) {
return const BiometricPolicy(); // Fall back to local config
}
}
}
Wire them in:
final shield = BiometricShield(
config: BiometricConfig(
tokenLifecycle: FirebaseTokenLifecycle(),
policyProvider: AppPolicyProvider(apiClient),
),
);
See doc/INTEGRATION.md for complete Firebase, Supabase, and REST JWT walkthroughs.
Common patterns
Gate on app launch
final result = await shield.authenticate(reason: 'Unlock', userId: user.id);
Protect a sensitive action
final result = await shield.authenticate(
reason: 'Confirm transfer',
requireFresh: true, // Ignores active session, forces re-auth
);
Silent session check
final result = await shield.validateOrAuthenticate(reason: 'Verify session');
// Only prompts if session is expired
Reactive session stream
StreamBuilder<BiometricSession?>(
stream: shield.sessionStream(userId: user.id),
builder: (context, snapshot) {
final session = snapshot.data;
if (session == null || session.isExpired) return LockScreen();
return Text('Expires in ${session.remainingValidity.inMinutes}m');
},
)
Custom fallback handler
class MyPinHandler extends FallbackHandler {
final BuildContext context;
MyPinHandler(this.context);
@override
Future<FallbackResult> handleFallback({
required BiometricFallback type,
required String reason,
}) async {
final ok = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => MyPinScreen(reason: reason)),
);
return ok == true ? FallbackResult.success : FallbackResult.cancelled;
}
}
User preferences (settings screen)
await shield.preferences.setBiometricEnabled(false, userId: user.id);
await shield.preferences.setSessionDurationOverride(
Duration(minutes: 30), userId: user.id,
);
Testing
The SDK ships with first-class testing support. No WidgetsFlutterBinding needed.
import 'package:biometric_shield/biometric_shield_testing.dart';
final mock = BiometricShieldMock(
authenticateResult: FakeBiometricResult.success(token: 'test-jwt'),
);
// Pass to your widget or service under test
final result = await mock.authenticate(reason: 'Test');
expect(result, isA<BiometricSuccess>());
// Verify calls
expect(mock.authenticateCalls, hasLength(1));
expect(mock.authenticateCalls.first.reason, 'Test');
Available test helpers: BiometricShieldMock, FakeBiometricResult, FakeBiometricSession, FakeTokenStore, BiometricTestConfig.
API reference
| Method | Description |
|---|---|
authenticate() |
Full auth flow with fallbacks |
validateOrAuthenticate() |
Silent check, prompts only if expired |
hasValidSession() |
Boolean session check |
sessionStream() |
Reactive session state stream |
getCapability() |
Device biometric capabilities |
storeToken() / getToken() |
Secure token storage (per-user) |
clearSession() / clearAll() |
Session and data cleanup |
getLockoutState() / resetLockout() |
Lockout management |
onActivity() |
Extend session on interaction |
preferences |
User-facing settings (enable/disable, timeout, etc.) |
dispose() / disposeUser() |
Resource cleanup |
What this SDK does NOT do
- Server authentication of any kind
- Token refresh against a server (unless you provide a
TokenLifecycle) - User registration or biometric enrollment
- OAuth / PKCE flows
- Push notification auth
- Navigation decisions
- Show any UI in response to results (that's your job via
.when())
Dependencies
| Package | Purpose |
|---|---|
local_auth |
Platform biometric invocation |
flutter_secure_storage |
Default secure token storage |
crypto |
Session ID generation |
universal_io |
Cross-platform detection |
Four dependencies. That's it.
License
MIT — see LICENSE.
Contributing
See CONTRIBUTING.md for development setup and contribution guidelines.
Built by Vipin Kumar Kashyap — SOUL.md is the design source of truth.
Libraries
- biometric_shield
- BiometricShield — A composable Flutter SDK for biometric authentication.
- biometric_shield_testing
- Testing utilities for the BiometricShield SDK.
- biometric_shield_ui
- BiometricShield Flutter UI layer.