🎯 Learning Path:
Welcome to the REL-ID Server Biometric Step-Up Authentication codelab! This tutorial enhances your notification application with high-security biometric verification using liveness detection for sensitive notification actions.
In this codelab, you'll enhance your existing notification application with:
By completing this codelab, you'll master:
Before starting this codelab, ensure you have:
The code to get started can be found in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-flutter.git
Navigate to the relid-IDV-post-login-step-up-auth-notification folder in the repository you cloned earlier
This codelab adds server biometric step-up authentication to your notification application with three core enhancements:
getIDVSelfieProcessStartConfirmation handler for workflow 9 with callback preservation patternBefore implementing server biometric authentication, let's understand the key differences from password step-up and the unique event flow.
The password step-up codelab (prerequisite) uses Challenge Mode 3 for re-authentication. Server biometric authentication uses a different mechanism:
Aspect | Password Step-Up (Prerequisite) | Server Biometric Step-Up (This Codelab) |
Backend Configuration |
|
|
SDK Event Triggered |
|
|
Event Parameter |
|
|
UI Pattern | Dialog widget (StepUpPasswordDialog) | Full screen navigation (IDVSelfieProcessStart) |
Authentication Method | Password input verification | Selfie capture with liveness detection |
Backend Verification | Password hash verification | Biometric template matching |
Security Level | Standard re-authentication | High-security with anti-spoofing |
The server biometric step-up authentication follows this event-driven pattern:
User Taps Notification Action → updateNotification(uuid, action) API Called →
Backend Determines authLevel = 4 (Server Biometric Required) →
SDK Triggers: getIDVSelfieProcessStartConfirmation Event (idvWorkflow: 9) →
[Screen-Level Handler Executes FIRST]:
_handleSelfieProcessStartForNotification() →
Checks data parameter (workflow info) →
Calls setState(() { _showActionModal = false }) → Action Modal Closes →
[Global Handler Executes SECOND]:
_originalGetSelfieHandler() (from SDKIDVEventProvider) →
Navigates to IDVSelfieProcessStart Screen using GoRouter →
User Reviews Guidelines → Taps "Start Selfie Capture" →
SDK Captures Selfie → Liveness Detection (Aware FaceCapture) →
SDK Verifies Biometric Template on Server →
SDK Triggers: onUpdateNotification Event →
[Provider-Level Handler Executes]:
_handleUpdateNotification() (in sdk_event_provider.dart) →
Navigates back to GetNotificationsScreen via appRouter.goNamed() →
Sets notificationStepUpResultProvider state after 300ms delay →
[Screen-Level Build Method Watches Provider]:
ref.watch(notificationStepUpResultProvider) detects change →
Calls _handleNotificationUpdateResult() after 100ms →
Displays Success/Error Dialog →
Navigates to Dashboard
Important: The authentication method is determined by the authLevel configured in the backend for each notification action:
AuthLevel | Authentication Method | SDK Behavior | Use Case |
1 | Password | SDK triggers | Standard security actions (prerequisite codelab) |
3 | LDA (Local Device Auth) | SDK handles biometric internally | Standard biometric actions |
4 | Server Biometric | SDK triggers | High-security actions requiring liveness |
IDV Workflow 9 is specifically designed for notification step-up authentication:
IDV Workflow | Purpose | When Used | Screen Navigation |
9 | Notification Step-Up Authentication | Sensitive notification actions with authLevel=4 | Must close action modal first |
10 | Biometric Opt-In | User enrolls in biometric authentication | Standalone flow |
6 | Post-Login KYC | Know Your Customer verification | Standalone flow |
8 | Post-Login Selfie | Biometric authentication after login | Standalone flow |
15 | Login Selfie | Biometric verification during login | Pre-login flow |
The REL-ID SDK uses these components for server biometric step-up:
Component | Description | Developer Action |
Aware FaceCapture Library | Embedded in rdna_client for liveness detection | Pre-configured, no action needed |
getIDVSelfieProcessStartConfirmation | SDK event when selfie capture is required (idvWorkflow: 9) | Register handler in GetNotificationsScreen |
IDVSelfieProcessStart Screen | Selfie capture initiation screen with guidelines | Already implemented in project |
onUpdateNotification | SDK event with authentication result | Provider-level handler sets state, screen watches with ref.watch() |
Now let's implement the screen-level event handler that intercepts getIDVSelfieProcessStartConfirmation events for workflow 9.
For server biometric step-up, we need two actions when the selfie event is triggered:
This requires a handler chaining pattern where our screen handler executes first, then calls the global handler:
// Screen handler executes first
_handleSelfieProcessStartForNotification(data); // Close modal
// Then call global handler
if (_originalGetSelfieHandler != null) {
_originalGetSelfieHandler!(data); // Navigate to selfie screen
}
Add the handler that intercepts workflow 9 events to close the action modal:
// lib/tutorial/screens/notification/get_notifications_screen.dart
// Add imports at the top
import 'package:relidcodelab/uniken/services/idv/rdna_idv_service.dart';
/// Handle selfie process start confirmation for notification step-up
///
/// This is triggered when server biometric step-up starts (idvWorkflow 9).
/// Closes the action modal to allow selfie screen to appear on top.
void _handleSelfieProcessStartForNotification(String eventData) {
// Close action modal if it's showing
if (mounted && _showActionModal) {
setState(() {
_showActionModal = false;
});
}
}
Preserve the original handler and chain it in _setupEventHandlers():
// lib/tutorial/screens/notification/get_notifications_screen.dart
// Add state variables for IDV event manager and original handler
class _GetNotificationsScreenState extends ConsumerState<GetNotificationsScreen> {
// ... existing variables ...
// NEW: IDV event manager and handler preservation
late final RdnaIDVEventManager _rdnaIDVEventManager;
RDNAIDVGetSelfieProcessStartConfirmationCallback? _originalGetSelfieHandler;
@override
void initState() {
super.initState();
// Initialize service references
final rdnaService = ref.read(rdnaServiceProvider);
_rdnaEventManager = rdnaService.getEventManager();
// NEW: Initialize IDV event manager
final rdnaIDVService = RdnaIDVService.getInstance();
_rdnaIDVEventManager = rdnaIDVService.getEventManager();
_setupEventHandlers();
_loadNotifications();
}
/// Set up event handlers for notification events
///
/// Updated: Now includes IDV selfie handler registration for server biometric step-up
void _setupEventHandlers() {
// Register notification handlers
_rdnaEventManager.setGetNotificationsHandler(_handleNotificationsReceived);
// NOTE: updateNotificationHandler is registered at SDK provider level (sdk_event_provider.dart)
// It sets notificationStepUpResultProvider which this screen watches in build()
// Preserve original getPassword handler and chain it
// This is critical - we need to handle challengeMode 3 but pass others to SDKEventProvider
_originalGetPasswordHandler = _rdnaEventManager.getPasswordHandler;
_rdnaEventManager.setGetPasswordHandler((RDNAGetPassword data) {
_handleGetPasswordStepUp(data);
});
// NEW: Register IDV selfie handler for server biometric step-up (idvWorkFlow: 9)
print('GetNotificationsScreen - Registering IDV selfie handler');
_originalGetSelfieHandler = _rdnaIDVEventManager.getSelfieProcessStartConfirmationHandler;
print('GetNotificationsScreen - Original selfie handler exists: ${_originalGetSelfieHandler != null}');
_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler((String data) {
// First handle notification-specific logic (close modal)
_handleSelfieProcessStartForNotification(data);
// Then call original handler for navigation
if (_originalGetSelfieHandler != null) {
_originalGetSelfieHandler!(data);
}
});
print('GetNotificationsScreen - IDV selfie handler registered');
}
Restore the original handler when the screen is disposed:
// lib/tutorial/screens/notification/get_notifications_screen.dart
/// Cleanup event handlers
void _cleanupEventHandlers() {
_rdnaEventManager.setGetNotificationsHandler(null);
// NOTE: updateNotificationHandler is managed at SDK provider level,
// so we don't cleanup here. It stays active throughout app lifecycle.
// Restore original getPassword handler
_rdnaEventManager.setGetPasswordHandler(_originalGetPasswordHandler);
// NEW: Restore original IDV selfie handler
_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler(_originalGetSelfieHandler);
print('GetNotificationsScreen - Handlers cleaned up (updateNotification kept active)');
}
Now let's understand how the enhanced provider-level handler manages screen closure and result delivery.
After server biometric verification completes, the onUpdateNotification event is triggered. At this point, the selfie screen (IDVSelfieProcessStart) may still be visible on top of the GetNotificationsScreen.
The provider navigator pattern handles this at the provider level:
sdk_event_provider.dart) executes when event firesappRouter.goNamed('getNotificationsScreen') to return to notifications screenref.watch() to detect resultThe architecture ensures that:
Provider Level (sdk_event_provider.dart)
├─ _handleUpdateNotification()
│ ├─ appRouter.goNamed('getNotificationsScreen') → Navigate back
│ └─ Future.delayed(300ms) → Set notificationStepUpResultProvider
│
Screen Level (get_notifications_screen.dart)
└─ build() watches notificationStepUpResultProvider
└─ _handleNotificationUpdateResult() → Show dialog
The provider-level handler in sdk_event_provider.dart is already implemented:
// lib/uniken/providers/sdk_event_provider.dart
/// Handle update notification response
///
/// This is the provider-level handler for notification updates.
/// It navigates back to GetNotificationsScreen and sets provider state.
void _handleUpdateNotification(RDNAStatusUpdateNotification data) {
print('SDKEventProvider - Received update notification event');
final sessionData = ref.read(sessionDataProvider);
// Navigate back to GetNotificationsScreen using global router
// This works even if the screen was disposed during selfie flow
appRouter.goNamed('getNotificationsScreen', extra: sessionData);
// Set provider state after delay to allow screen transition
Future.delayed(const Duration(milliseconds: 300), () {
ref.read(notificationStepUpResultProvider.notifier).state = data;
});
}
The GetNotificationsScreen build method watches the provider and processes results:
// lib/tutorial/screens/notification/get_notifications_screen.dart
@override
Widget build(BuildContext context) {
// Watch provider for notification step-up result (similar to BiometricOptInScreen pattern)
// SDK provider closes the selfie screen and sets state after 300ms delay
final stepUpResult = ref.watch(notificationStepUpResultProvider);
// Handle step-up result when provider state changes
if (stepUpResult != null) {
print('GetNotificationsScreen - Step-up result detected from provider');
// Clear provider state immediately
Future.microtask(() {
ref.read(notificationStepUpResultProvider.notifier).state = null;
});
// Show result (SDK provider already closed selfie screen)
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_handleNotificationUpdateResult(stepUpResult);
}
});
}
return Stack(
children: [
Scaffold(...), // Main content
if (_showActionModal) _buildActionModal(), // Action modal
StepUpPasswordDialog(...) // Step-up auth dialog
],
);
}
The _handleNotificationUpdateResult() method processes the notification update response:
// lib/tutorial/screens/notification/get_notifications_screen.dart
/// Handle notification update result
///
/// Processes the notification update response and shows appropriate dialogs
void _handleNotificationUpdateResult(RDNAStatusUpdateNotification data) {
print('GetNotificationsScreen - Processing notification update result');
// Check for errors first
if (data.error.longErrorCode != 0) {
final errorMessage = data.error.errorString ?? 'Failed to update notification';
if (mounted) {
setState(() {
_showStepUpAuth = false;
});
_showErrorDialog(errorMessage);
}
return;
}
final responseData = data.pArgs?.response;
final statusCode = responseData?.StatusCode;
final statusMessage = responseData?.StatusMsg ?? 'Unknown error occurred';
if (statusCode == 100) {
// Success - notification action completed
if (mounted) {
setState(() {
_showStepUpAuth = false;
_showActionModal = false;
});
_showSuccessDialog(
statusMessage ?? 'Notification action completed successfully',
onOk: () {
context.go('/dashboard', extra: widget.sessionData);
}
);
_loadNotifications();
}
} else if (statusCode == 110 || statusCode == 153) {
// Critical errors - show alert BEFORE SDK logout
if (mounted) {
setState(() {
_showStepUpAuth = false;
_showActionModal = false;
});
_showErrorDialog(statusMessage);
}
} else {
// Other errors
if (mounted) {
setState(() {
_showStepUpAuth = false;
});
_showErrorDialog(statusMessage);
}
}
}
Now let's test the complete server biometric step-up implementation.
Before testing, ensure your REL-ID server is configured for server biometric step-up:
Test the complete server biometric step-up flow:
getNotifications() succeededConsole logs should show:
- "GetNotificationsScreen - Calling updateNotification API"
- "GetNotificationsScreen - Registering IDV selfie handler"
- "GetNotificationsScreen - Original selfie handler exists: true"
- "GetNotificationsScreen - IDV selfie handler registered"
- "SDKIDVEventProvider - Selfie process start confirmation requested"
Console logs should show:
- "SDKEventProvider - Received update notification event"
- "GetNotificationsScreen - Step-up result detected from provider"
- "GetNotificationsScreen - Processing notification update result"
Test user cancellation during selfie capture:
Test server biometric verification failure:
Use this checklist to verify your implementation:
Let's understand the architecture decisions and troubleshoot common issues.
Provider-level approach (better): Use appRouter.goNamed() in SDK provider
Advantages of provider-level navigation:
// ✅ Better: Provider-level navigation
void _handleUpdateNotification(RDNAStatusUpdateNotification data) {
appRouter.goNamed('getNotificationsScreen', extra: sessionData);
Future.delayed(const Duration(milliseconds: 300), () {
ref.read(notificationStepUpResultProvider.notifier).state = data;
});
}
// ❌ Worse: Screen-level navigation
void _handleUpdateNotificationReceived(RDNAStatusUpdateNotification data) {
context.go('/get-notifications'); // May fail if screen disposed
}
Current approach (better): Preserve original handler and chain it
Advantages of preservation pattern:
// ✅ Better: Preservation and chaining
_originalGetSelfieHandler = _rdnaIDVEventManager.getSelfieProcessStartConfirmationHandler;
_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler((String data) {
_handleSelfieProcessStartForNotification(data); // Our logic
if (_originalGetSelfieHandler != null) {
_originalGetSelfieHandler!(data); // SDK logic
}
});
// ❌ Worse: Direct replacement
_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler((String data) {
_handleSelfieProcessStartForNotification(data);
// Navigation broken - SDK handler lost!
});
Issue 1: Action Modal Not Hiding
Symptoms: Selfie screen appears behind action modal
Solution: Verify handler registration and setState() call
// Check console logs:
// - "GetNotificationsScreen - Registering IDV selfie handler"
// - "GetNotificationsScreen - Original selfie handler exists: true"
// - "GetNotificationsScreen - IDV selfie handler registered"
// Ensure setState() is called
void _handleSelfieProcessStartForNotification(String eventData) {
if (mounted && _showActionModal) {
setState(() {
_showActionModal = false; // CRITICAL: Must use setState
});
}
}
Issue 2: Dialog Doesn't Appear After Biometric
Symptoms: Navigation works but no success/error dialog shown
Solution: Ensure provider is being watched in build() method
// In build() method:
final stepUpResult = ref.watch(notificationStepUpResultProvider);
if (stepUpResult != null) {
Future.microtask(() {
ref.read(notificationStepUpResultProvider.notifier).state = null;
});
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_handleNotificationUpdateResult(stepUpResult);
}
});
}
Issue 3: Handler Not Triggering for Workflow 9
Symptoms: Console logs show selfie event but handler not executing
Solution: Verify IDV event manager initialization
// ✅ Correct initialization in initState()
late final RdnaIDVEventManager _rdnaIDVEventManager;
@override
void initState() {
super.initState();
final rdnaIDVService = RdnaIDVService.getInstance();
_rdnaIDVEventManager = rdnaIDVService.getEventManager();
_setupEventHandlers();
}
// ❌ Wrong - using ref in handler (causes disposal errors)
void _setupEventHandlers() {
final rdnaIDVService = ref.read(rdnaIDVServiceProvider); // Don't do this!
}
Issue 4: Other IDV Workflows Broken
Symptoms: Biometric opt-in, KYC flows not working after implementation
Solution: Ensure original handler is called in chain
_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler((String data) {
_handleSelfieProcessStartForNotification(data);
// CRITICAL: Must call original handler
if (_originalGetSelfieHandler != null) {
_originalGetSelfieHandler!(data);
}
});
Let's review important security considerations for server biometric authentication.
Never expose biometric templates:
// ❌ Wrong - Logging biometric data
print('Biometric template: ${selfieData.template}');
// ✅ Correct - Only log workflow and status
print('Server biometric verification: ${data.pArgs?.response?.StatusCode == 100}');
Trust SDK's liveness detection:
The Aware FaceCapture library embedded in rdna_client provides:
Never bypass authLevel requirements:
// ❌ Wrong - Trying to bypass server biometric
if (requiresServerBiometric) {
// Don't call updateNotification again to skip auth
}
// ✅ Correct - Always respect backend authLevel
try {
final syncResponse = await rdnaService.updateNotification(uuid, action);
// Let SDK handle authLevel=4 requirement via events
} catch (error) {
// Handle API errors only
}
Log security-relevant events:
// ✅ Correct - Audit logging
print('Server biometric step-up initiated: $notificationUUID, $action, ${DateTime.now().toIso8601String()}');
print('Server biometric verification result: ${statusCode == 100}, $statusCode, ${DateTime.now().toIso8601String()}');
Compliance considerations:
Congratulations! You've successfully implemented server biometric step-up authentication for notification actions with REL-ID SDK.
In this codelab, you've learned how to:
✅ Understand AuthLevel Configuration: Learned how authLevel=4 triggers server biometric (vs challengeMode 3 for password)
✅ Implement IDV Workflow 9: Handled notification-specific selfie capture with liveness detection
✅ Use Handler Preservation Pattern: Applied callback preservation with chaining for reliable event handling
✅ Chain Event Handlers: Preserved global navigation while adding screen-specific logic (close modal)
✅ Manage Modal Lifecycle: Closed action modal with setState() before selfie screen to prevent UI blocking
✅ Use Provider Navigator Pattern: Implemented provider-level navigation with GoRouter for cross-hierarchy screen management
✅ Implement Screen Transition Timing: Used Future.delayed() for smooth UI updates before dialogs
✅ Integrate Liveness Detection: Leveraged Aware FaceCapture library for anti-spoofing
AuthLevel vs Challenge Mode:
authLevel = 1, challengeMode = 3, dialog widgetauthLevel = 4, idvWorkflow = 9, full screen selfie captureHandler Preservation Pattern:
_originalGetSelfieHandler = _rdnaIDVEventManager.getSelfieProcessStartConfirmationHandler_rdnaIDVEventManager.setGetSelfieProcessStartConfirmationHandler(_originalGetSelfieHandler)Provider Navigator Pattern:
appRouter.goNamed()Future.delayed(300ms)ref.watch()Future.microtask()Handler Chaining:
Thank you for completing this codelab! You now have the knowledge to implement secure, production-ready server biometric step-up authentication for notification actions in your Flutter applications.
Happy Coding! 🚀