🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Additional Device Activation Flow With Notifications Codelab
  3. Complete REL-ID Step-Up Authentication with Notifications (Password)
  4. You are here → Server Biometric Step-Up Authentication with IDV Workflow 9

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.

What You'll Build

In this codelab, you'll enhance your existing notification application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Server Biometric Flow: Understanding authLevel=4 and how it differs from password (challengeMode 3)
  2. IDV Workflow 9: Implementing notification-specific selfie capture with liveness detection
  3. Event Handler Registration: Using initState() with handler preservation pattern for reliable handler registration
  4. Handler Chaining: Preserving global handlers while adding screen-specific logic using callback preservation
  5. Modal Lifecycle Management: Closing action modal with setState() before selfie screen appears
  6. Provider Navigator Pattern: Using provider-level navigation with GoRouter for cross-hierarchy screen management
  7. Screen Transition Delays: Ensuring smooth UI updates with proper timing using Future.delayed()
  8. Liveness Detection: Integrating Aware FaceCapture library for anti-spoofing

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

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

Codelab Architecture Overview

This codelab adds server biometric step-up authentication to your notification application with three core enhancements:

  1. IDV Event Handler: getIDVSelfieProcessStartConfirmation handler for workflow 9 with callback preservation pattern
  2. Action Modal Management: Close action modal with setState() before selfie screen appears to prevent UI blocking
  3. Provider Navigator Pattern: Provider-level navigation using GoRouter for smooth screen transitions and result handling

Before implementing server biometric authentication, let's understand the key differences from password step-up and the unique event flow.

Password vs Server Biometric Step-Up

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

authLevel = 1

authLevel = 4

SDK Event Triggered

getPassword event

getIDVSelfieProcessStartConfirmation event

Event Parameter

challengeMode = 3

idvWorkflow = 9

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

Server Biometric Event Flow

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

AuthLevel Configuration

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 getPassword with challengeMode 3

Standard security actions (prerequisite codelab)

3

LDA (Local Device Auth)

SDK handles biometric internally

Standard biometric actions

4

Server Biometric

SDK triggers getIDVSelfieProcessStartConfirmation with idvWorkflow 9

High-security actions requiring liveness

IDV Workflow 9 - Notification Step-Up

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

Key Server Biometric Components

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.

Understanding Handler Chaining Pattern

For server biometric step-up, we need two actions when the selfie event is triggered:

  1. Screen-Level Logic: Close the action modal to prevent UI blocking
  2. Global Navigation: Navigate to IDVSelfieProcessStart screen

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
}

Implement Selfie Process Start Handler

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;
    });
  }
}

Register Handler with Preservation Pattern

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');
  }

Add Cleanup in dispose()

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.

Understanding Provider Navigator Pattern

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:

  1. Provider-Level Handler (in sdk_event_provider.dart) executes when event fires
  2. Navigate Back using appRouter.goNamed('getNotificationsScreen') to return to notifications screen
  3. Set Provider State with notification result after 300ms delay
  4. Screen Watches Provider with ref.watch() to detect result
  5. Display Dialog on GetNotificationsScreen after screen is visible

The 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

Provider-Level Handler (Already Implemented)

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;
  });
}

Screen-Level Result Watching (Already Implemented)

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
    ],
  );
}

Result Processing Logic

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.

Server Configuration

Before testing, ensure your REL-ID server is configured for server biometric step-up:

Test Scenario 1: Server Biometric Success Flow

Test the complete server biometric step-up flow:

  1. Complete MFA flow and log in to dashboard
  2. Navigate to Notifications screen from drawer menu
  3. Verify notifications loaded - Check that getNotifications() succeeded
  4. Tap notification with authLevel=4 - Select action requiring server biometric
  5. Verify action modal appears - Modal displays action details
  6. Submit action - Tap action button
  7. Verify handler execution:
    Console 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"
    
  8. Verify modal closes - Action modal should close automatically via setState()
  9. Verify selfie screen appears - IDVSelfieProcessStart screen on top
  10. Review workflow 9 guidelines - Screen shows notification step-up specific text
  11. Tap "Start Selfie Capture" - Initiate biometric capture
  12. Complete selfie capture - SDK captures selfie with liveness detection
  13. Verify result handling:
    Console logs should show:
    - "SDKEventProvider - Received update notification event"
    - "GetNotificationsScreen - Step-up result detected from provider"
    - "GetNotificationsScreen - Processing notification update result"
    
  14. Verify screen navigation - Provider navigates back to GetNotificationsScreen
  15. Verify dialog appears - Success dialog displays on GetNotificationsScreen
  16. Navigate to dashboard - Tap "OK" button
  17. Verify notifications refresh - Latest notification list loaded

Test Scenario 2: Server Biometric with User Cancellation

Test user cancellation during selfie capture:

  1. Steps 1-9 - Same as Scenario 1 (up to selfie screen appearing)
  2. Tap Cancel/Back - User decides not to complete biometric
  3. Verify selfie screen closes - Returns to GetNotificationsScreen
  4. Verify action modal state - User can retry action if needed

Test Scenario 3: Server Biometric Failure

Test server biometric verification failure:

  1. Steps 1-11 - Same as Scenario 1 (up to selfie capture)
  2. Complete poor quality selfie - Intentionally provide poor lighting/angle
  3. SDK returns failure - Server verification fails
  4. Verify error handling:
    • Provider navigates back to GetNotificationsScreen
    • Error dialog displays: "Biometric verification failed"
    • User can retry action from notifications screen

Verification Checklist

Use this checklist to verify your implementation:

Let's understand the architecture decisions and troubleshoot common issues.

Why Provider-Level Navigation Instead of Screen-Level?

Provider-level approach (better): Use appRouter.goNamed() in SDK provider

Advantages of provider-level navigation:

  1. Survives screen disposal: Provider outlives individual screens
  2. No context issues: Uses global router instead of screen context
  3. Consistent navigation: Single navigation source regardless of screen state
  4. No race conditions: Provider always receives events, even if screen is disposed
// ✅ 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
}

Why Handler Preservation Instead of Direct Registration?

Current approach (better): Preserve original handler and chain it

Advantages of preservation pattern:

  1. No broken navigation: Global handler still executes for screen navigation
  2. Graceful cleanup: Can restore original handler on dispose
  3. No side effects: Other IDV workflows still work correctly
  4. Explicit chaining: Clear order of execution (screen first, then global)
// ✅ 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!
});

Common Issues and Solutions

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.

Biometric Template Security

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}');

Liveness Detection

Trust SDK's liveness detection:

The Aware FaceCapture library embedded in rdna_client provides:

AuthLevel Enforcement

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
}

Compliance and Audit

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.

What You've Accomplished

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

Key Takeaways

AuthLevel vs Challenge Mode:

Handler Preservation Pattern:

Provider Navigator Pattern:

Handler Chaining:

Additional Resources

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! 🚀