🎯 Learning Path:
Welcome to the REL-ID IDV Post-Login Server Biometric Authentication Flow codelab! This tutorial teaches you how to implement secure server-side biometric verification using stored biometric templates with liveness detection for activated customers.
In this codelab, you'll implement a complete post-login server biometric authentication workflow:
This codelab demonstrates post-login server biometric authentication, which is different from KYC:
KYC Flow (Workflow 6, Challenge Mode 9):
Document Scan → Selfie Capture → Biometric Consent → Complete KYC
(Creates new biometric template)
Server Biometric Auth Flow (Workflow 8, Challenge Mode 25 - This Codelab):
Template Check → Start Authentication → Selfie Capture → View Result
(Uses existing biometric template for verification)
Critical Differences:
onIDVServerBiometricAuthenticationResult instead of onActivatedCustomerKYCResponseUnderstanding the complete flow is crucial before implementation:
1. User Login (Activated Customer with Biometric Template)
↓ (Existing credentials authenticated)
2. Dashboard → Open Menu
↓ (User navigates to Biometric Authentication option)
3. 🆕 Server Biometric Authentication Screen
├─ viewDidLoad() triggers
├─ Automatic template status check
├─ checkIDVUserBiometricTemplateStatus() API called
└─ Wait for onIDVCheckUserBiometricTemplateStatus callback
↓
4. Template Status Validation
├─ onIDVCheckUserBiometricTemplateStatus callback received
├─ View controller handler processes callback
├─ Parse status code:
│ ├─ statusCode === 100: Template exists → Enable button
│ └─ statusCode === 600: No template → Disable button, show message
└─ Display template status on screen
↓
5. Start Authentication (only if template exists)
├─ User taps "Start Authentication" button
├─ initiateIDVServerBiometricAuthentication("Server Biometric Authentication") API called
└─ SDK initiates server biometric authentication workflow
↓
6. 📸 Selfie Capture Process (Reused from IDV MFA Flow)
├─ getIDVSelfieProcessStartConfirmation callback
│ • idvWorkFlow: 8 (selfie capture)
├─ Navigate to IDVSelfieProcessStartConfirmationViewController
├─ User taps "Start Selfie Capture" OR Close button (X)
│ ├─ "Start Selfie Capture": setIDVSelfieProcessStartConfirmation(true, 8) → Opens camera
│ └─ Close button: setIDVSelfieProcessStartConfirmation(false, 8) → Triggers result with error 240
└─ Native Selfie Capture (Aware FaceCapture) with liveness detection
↓
7. ✅ Server Biometric Authentication Result
├─ onIDVServerBiometricAuthenticationResult callback triggered
│ • challengeMode: 25 (server biometric authentication)
├─ View controller handler receives callback (even if on different screen)
├─ Pop back to ServerBiometricAuthentication view controller
│ • navigationController?.popToViewController(self, animated: true)
├─ Wait 500ms for navigation to complete
├─ Parse idvResponse for audit data:
│ ├─ Authentication Status (MATCHED/NOT_MATCHED from biometric_result.display_text)
│ ├─ Orchestration Use Case (RDNA_IDV_POSTLOGIN_IDV_SELFIE_BIOMETRIC)
│ ├─ Transaction ID (idv_audit_transaction_id)
│ └─ Attempts Left (attempts_left)
├─ Display result alert with audit data
└─ User taps "Back to Dashboard" or "OK"
↓
8. Completion
├─ Navigate to Dashboard or stay on screen
└─ User can perform another authentication (template check happens on viewWillAppear)
The biometric authentication flow is accessed from the menu:

The menu includes a "🔐 Biometric Authentication" menu item that navigates to the ServerBiometricAuthenticationViewController.
For post-login server biometric authentication, you'll use these specific values:
Parameter | Value | Description |
idvWorkflow |
| Selfie capture for server biometric authentication |
challengeMode |
| Server biometric authentication result callback |
Status Codes |
| Template exists / No template |
Comparison with Other IDV Flows:
Aspect | KYC Flow | Additional Document Scan | Server Biometric Auth |
Workflow | 6 | 7 | 8 |
Challenge Mode | 9 | 10 | 25 |
Template Check | No | No | Yes (required) |
Document Scan | Yes | Yes | No |
Selfie Capture | Yes | No | Yes |
Biometric Consent | Yes | No | No |
Template Creation | Yes (new) | No | No (uses existing) |
Entry Point | ActivatedCustomerKYCViewController | AdditionalDocumentScanViewController | ServerBiometricAuthenticationViewController |
API | initiateActivatedCustomerKYC | initiateIDVAdditionalDocumentScan | initiateIDVServerBiometricAuthentication and checkIDVUserBiometricTemplateStatus |
Result Callback | onActivatedCustomerKYCResponse | onIDVAdditionalDocumentScanResult | onIDVServerBiometricAuthenticationResult |
Ensure you have completed these requirements:
# Required tools
Xcode >= 13.0
CocoaPods or Swift Package Manager
iOS Deployment Target >= 13.0
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-team/relid-codelab-ios.git
Navigate to the relid-IDV-post-login-server-biometric-authentication folder:
cd relid-codelab-ios/relid-IDV-post-login-server-biometric-authentication
The post-login server biometric authentication codelab follows this structure:
relidcodelab/
├── AppDelegate.swift # App lifecycle
├── SceneDelegate.swift # Scene lifecycle (iOS 13+)
├── Sources/
│ ├── Tutorial/
│ │ ├── Screens/
│ │ │ ├── IDV/ # IDV Screens
│ │ │ │ ├── ServerBiometricAuthenticationViewController.swift # 🆕 Auth Entry Point
│ │ │ │ ├── IDVSelfieProcessStartConfirmationViewController.swift # ♻️ Reused
│ │ │ │ └── ...
│ │ │ ├── MFA/ # MFA Screens
│ │ │ │ ├── DashboardViewController.swift
│ │ │ │ └── ...
│ │ │ └── MenuViewController.swift # 🆕 Menu with Auth item
│ │ └── Navigation/
│ │ └── AppIDVCoordinator.swift # 🆕 Auth navigation
│ └── Uniken/
│ ├── Services/
│ │ ├── IDV/
│ │ │ ├── RDNAIDVService.swift # 🆕 Template check + auth APIs
│ │ │ │ # - checkIDVUserBiometricTemplateStatus()
│ │ │ │ # - initiateIDVServerBiometricAuthentication()
│ │ │ └── RDNAIDVDelegateManager.swift # 🆕 Callback handlers
│ │ │ # - onIDVCheckUserBiometricTemplateStatus
│ │ │ # - onIDVServerBiometricAuthenticationResult
│ │ └── RDNADelegateManager.swift
│ ├── Utils/
│ │ └── ConnectionProfileParser.swift # Bundle resource loading
│ └── CP/
│ └── agent_info.json # Connection profile
├── Base.lproj/
│ └── Main.storyboard # UI layout
├── Info.plist # App configuration
├── Podfile # CocoaPods dependencies
└── RELID_IDV/ # Local pod (SDK)
The project includes pre-configured Aware FaceCapture library for biometric authentication:
Before implementing the authentication screen, you need to add two API methods to the IDV service layer:
checkIDVUserBiometricTemplateStatus() - Check if user has biometric template on serverinitiateIDVServerBiometricAuthentication(reason:) - Start authentication workflowAdd the following method to RDNAIDVService.swift:
// Sources/Uniken/Services/IDV/RDNAIDVService.swift
import Foundation
import RELID
class RDNAIDVService {
// MARK: - Singleton
static let shared = RDNAIDVService()
private let rdna = RDNA.sharedInstance()
private init() {
print("RDNAIDVService - Service initialized")
}
// MARK: - Server Biometric Authentication APIs
/**
* Check IDV User Biometric Template Status
*
* Checks if the user has a biometric template stored on the server.
* This should be called before allowing server biometric authentication.
*
* @returns RDNAError indicating success or failure
*
* Flow:
* 1. SDK checks template availability on server
* 2. SDK triggers onIDVCheckUserBiometricTemplateStatus callback with status code:
* - 100 = Template present (user can authenticate)
* - 600 = Template does not exist (user must complete biometric opt-in first)
* - 400/500 = Error
*
* Success Response:
* - error.longErrorCode = 0
* - Wait for onIDVCheckUserBiometricTemplateStatus callback
*
* Error Responses:
* - Sync error: error.longErrorCode !== 0
*/
func checkIDVUserBiometricTemplateStatus() -> RDNAError {
print("RDNAIDVService - Checking user biometric template status")
let error = rdna.checkIDVUserBiometricTemplateStatus()
if error.longErrorCode == 0 {
print("RDNAIDVService - CheckIDVUserBiometricTemplateStatus request successful")
print(" Awaiting onIDVCheckUserBiometricTemplateStatus callback")
} else {
print("RDNAIDVService - CheckIDVUserBiometricTemplateStatus failed:")
print(" Error: \(error.errorString)")
print(" Code: \(error.longErrorCode)")
}
return error
}
// ... continue with authentication initiation API
}
Add the following method to RDNAIDVService.swift:
/**
* Initiate IDV Server Biometric Authentication
*
* Initiates post-login server biometric authentication workflow for already activated
* customers with stored biometric templates.
*
* @param reason - Reason for authentication (e.g., "Server Biometric Authentication")
* @returns RDNAError indicating success or failure
*
* Flow:
* 1. SDK initiates server biometric authentication workflow
* - workflow: 8 (selfie capture)
* - challengeMode: 25 (server biometric auth result)
* 2. SDK automatically triggers:
* - getIDVSelfieProcessStartConfirmation callback (idvWorkFlow: 8)
* 3. After selfie capture, SDK triggers:
* - onIDVServerBiometricAuthenticationResult callback (challengeMode: 25)
*
* Success Response:
* - error.longErrorCode = 0
*
* Error Responses:
* - Sync error: error.longErrorCode !== 0
* - Backend error: Received in onIDVServerBiometricAuthenticationResult callback
*/
func initiateIDVServerBiometricAuthentication(reason: String) -> RDNAError {
print("RDNAIDVService - Initiating IDV server biometric authentication")
print(" Reason: \(reason)")
let error = rdna.initiateIDVServerBiometricAuthentication(reason)
if error.longErrorCode == 0 {
print("RDNAIDVService - InitiateIDVServerBiometricAuthentication request successful")
print(" Awaiting onIDVServerBiometricAuthenticationResult callback")
} else {
print("RDNAIDVService - InitiateIDVServerBiometricAuthentication failed:")
print(" Error: \(error.errorString)")
print(" Code: \(error.longErrorCode)")
}
return error
}
}
Implement callback handlers for template status check and authentication result in the IDV Delegate Manager using closures.
Add the following to RDNAIDVDelegateManager.swift:
// Sources/Uniken/Services/IDV/RDNAIDVDelegateManager.swift
import Foundation
import RELID
/// Manages IDV-specific delegate callbacks from the RELID SDK
class RDNAIDVDelegateManager: NSObject {
// MARK: - Singleton
static let shared = RDNAIDVDelegateManager()
private override init() {
super.init()
}
// MARK: - Server Biometric Authentication Callback Closures
/// Closure invoked when biometric template status check completes
var onIDVCheckUserBiometricTemplateStatus: ((_ userID: String, _ idvResponse: String, _ status: RDNARequestStatus, _ error: RDNAError) -> Void)?
/// Closure invoked when server biometric authentication completes
var onIDVServerBiometricAuthenticationResult: ((_ userID: String, _ idvResponse: String, _ status: RDNARequestStatus, _ error: RDNAError) -> Void)?
// ... existing closures
}
Extend RDNAIDVDelegateManager to conform to RDNAIDVBiometricManagementCallbacks:
// MARK: - RDNAIDVBiometricManagementCallbacks Protocol
extension RDNAIDVDelegateManager: RDNAIDVBiometricManagementCallbacks {
/**
* Template Status Check Callback
* Triggered after checkIDVUserBiometricTemplateStatus API call
*/
func onIDVCheckUserBiometricTemplateStatus(
_ userID: String,
idvResponse: String,
status: RDNARequestStatus,
error: RDNAError
) {
print("RDNAIDVDelegateManager - onIDVCheckUserBiometricTemplateStatus called")
print(" User ID: \(userID)")
print(" Status Code: \(status.statusCode)")
print(" Error Code: \(error.longErrorCode)")
DispatchQueue.main.async { [weak self] in
self?.onIDVCheckUserBiometricTemplateStatus?(userID, idvResponse, status, error)
}
}
/**
* Server Biometric Authentication Result Callback
* Triggered after selfie capture completes during server biometric authentication
* Challenge Mode: 25
*/
func onIDVServerBiometricAuthenticationResult(
_ userID: String,
idvResponse: String,
status: RDNARequestStatus,
error: RDNAError
) {
print("RDNAIDVDelegateManager - onIDVServerBiometricAuthenticationResult called")
print(" User ID: \(userID)")
print(" Status Code: \(status.statusCode)")
print(" Error Code: \(error.longErrorCode)")
DispatchQueue.main.async { [weak self] in
self?.onIDVServerBiometricAuthenticationResult?(userID, idvResponse, status, error)
}
}
}
Ensure the delegate is registered in your initialization flow (in onInitialized callback):
// In your initialization completion handler
func onInitialized(_ response: RDNAChallengeResponse) {
// Register IDV delegates
let error = RDNAIDVService.shared.setIDVCallback()
if error.longErrorCode == 0 {
// Register specific delegates
RDNAIDVService.shared.setIDVBiometricManagementDelegate(RDNAIDVDelegateManager.shared)
print("IDV Biometric Management delegate registered")
}
}
[weak self] to avoid retain cyclesRDNAIDVBiometricManagementCallbacksThe iOS SDK provides callback methods that deliver authentication results through strongly-typed parameters. Understanding these structures is essential for parsing responses correctly.
When the SDK calls your onIDVCheckUserBiometricTemplateStatus callback, you receive:
func onIDVCheckUserBiometricTemplateStatus(
_ userID: String, // Current user identifier
idvResponse: String, // JSON string with detailed info
status: RDNARequestStatus, // Backend status (statusCode, statusMessage)
error: RDNAError // SDK error (longErrorCode, errorString)
)
Status Codes (in status.statusCode):
100 = Template present on server (user can authenticate)600 = Template does not exist (user must complete biometric opt-in first)400/500 = Error codesValidation Pattern:
error.longErrorCode first (0 = OK)status.statusCode (100 = exists, 600 = no template)idvResponse JSON for detailed informationThe idvResponse string contains JSON with this structure:
// Example idvResponse JSON structure
{
"status_code": 100, // Same as status.statusCode
"status_message": "Success",
"orchestration_use_case": "RDNA_IDV_CHECK_TEMPLATE",
"result": true, // true if template exists
"attempts_left": 5,
"user_id": "user123"
}
Parsing Example:
guard let data = idvResponse.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return
}
let statusCode = json["status_code"] as? Int ?? 0
let result = json["result"] as? Bool ?? false
let attemptsLeft = json["attempts_left"] as? Int ?? 0
When the SDK calls your onIDVServerBiometricAuthenticationResult callback, you receive:
func onIDVServerBiometricAuthenticationResult(
_ userID: String, // Current user identifier
idvResponse: String, // JSON with video data + audit info
status: RDNARequestStatus, // Backend status
error: RDNAError // SDK error
)
Challenge Mode: Always 25 for server biometric authentication
Validation Pattern:
error.longErrorCode first (0 = OK, 240 = user cancelled)status.statusCode (100 or 0 = success)idvResponse to check idv_audit_info.status (SUCCESS/FAILURE)The idvResponse contains complex JSON with video data and audit information:
// Example idvResponse JSON structure (partial)
{
"video": {
"workflow_data": { /* frames, rotation, workflow */ },
"meta_data": { /* device info */ },
"client_version": "1.0"
},
"idv_audit_info": {
"reason": "Server Biometric Authentication",
"audit_metrics": {
"start_timestamp": 1234567890,
"end_timestamp": 1234567900
},
"status": "SUCCESS", // ← CRITICAL: SUCCESS or FAILURE
"idv_audit_transaction_id": "txn_123", // ← Unique transaction ID
"orchestration_use_case": "RDNA_IDV_POSTLOGIN_IDV_SELFIE_BIOMETRIC"
},
"purpose": "Server Biometric Authentication",
"attempts": 1,
"user_id": "user123",
"attempts_left": 4
}
Parsing Example for Audit Info:
guard let data = idvResponse.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return
}
// Parse biometric result
let biometricResult = json["biometric_result"] as? [String: Any]
let displayText = biometricResult?["display_text"] as? String ?? "Unknown"
// Parse audit info
let auditInfo = json["idv_audit_info"] as? [String: Any]
let auditStatus = auditInfo?["status"] as? String ?? "UNKNOWN"
let transactionId = auditInfo?["idv_audit_transaction_id"] as? String ?? "N/A"
let useCase = auditInfo?["orchestration_use_case"] as? String ?? "N/A"
let attemptsLeft = json["attempts_left"] as? Int ?? 0
// Determine success
let isSuccess = displayText == "MATCHED"
Template Status Check:
status.statusCode: 100 (exists) or 600 (no template)idvResponse.result: Boolean indicating template existenceAuthentication Result:
error.longErrorCode: 0 (success), 240 (user cancelled), other (errors)status.statusCode: 100 or 0 (success)idvResponse.biometric_result.display_text: "MATCHED", "NOT_MATCHED", "LIVENESS_FAILED"idvResponse.idv_audit_info.status: "SUCCESS" or "FAILURE"idvResponse.idv_audit_info.idv_audit_transaction_id: Unique transaction identifierAlways validate in this order:
// Tier 1: Check SDK error
if error.longErrorCode != 0 {
// SDK error - handle immediately
print("SDK error: \(error.errorString)")
return
}
// Tier 2: Check backend status
if status.statusCode != 100 && status.statusCode != 0 {
// Backend error
print("Status error: \(status.statusMessage)")
return
}
// Tier 3: Parse idvResponse for detailed audit data
guard let json = parseJSON(idvResponse),
let auditStatus = getAuditStatus(json) else {
return
}
if auditStatus == "SUCCESS" {
// Handle success
} else {
// Handle failure with audit details
}
idv_audit_info.status fieldidvResponse stringsdisplay_text for MATCHED/NOT_MATCHEDImplement the main screen that handles template status check, authentication initiation, and result display. This screen uses the view lifecycle pattern for robust callback handling.
Create ServerBiometricAuthenticationViewController.swift:
// Sources/Tutorial/Screens/IDV/ServerBiometricAuthenticationViewController.swift
import UIKit
import RELID
class ServerBiometricAuthenticationViewController: UIViewController {
// MARK: - IBOutlets
@IBOutlet weak var menuButton: UIButton!
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var templateStatusCard: UIView!
@IBOutlet weak var templateStatusTitleLabel: UILabel!
@IBOutlet weak var templateStatusTextLabel: UILabel!
@IBOutlet weak var templateStatusCodeLabel: UILabel!
@IBOutlet weak var statusBannerView: UIView!
@IBOutlet weak var statusBannerLabel: UILabel!
@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var disabledNoteLabel: UILabel!
// MARK: - Properties
private var isCheckingStatus = false
private var isAuthenticating = false
private struct TemplateStatus {
var checked: Bool
var exists: Bool
var statusCode: Int
var statusMessage: String
}
private var templateStatus = TemplateStatus(
checked: false,
exists: false,
statusCode: 0,
statusMessage: ""
)
// ... continue with lifecycle
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupEventHandlers()
checkTemplateStatus()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Re-register event handlers when screen appears
setupEventHandlers()
// Reset button states
isAuthenticating = false
isCheckingStatus = false
updateButtonState()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Don't clear handlers here - they need to stay active during native UI
}
private func setupEventHandlers() {
print("ServerBiometricAuthenticationViewController - Setting up event handlers")
// Register closures with RDNAIDVDelegateManager
RDNAIDVDelegateManager.shared.onIDVCheckUserBiometricTemplateStatus = { [weak self] userID, idvResponse, status, error in
self?.handleCheckUserBiometricTemplateStatus(userID: userID, idvResponse: idvResponse, status: status, error: error)
}
RDNAIDVDelegateManager.shared.onIDVServerBiometricAuthenticationResult = { [weak self] userID, idvResponse, status, error in
self?.handleServerBiometricAuthenticationResult(userID: userID, idvResponse: idvResponse, status: status, error: error)
}
}
// MARK: - Template Status Check
private func checkTemplateStatus() {
print("ServerBiometricAuthenticationViewController - Checking template status")
isCheckingStatus = true
updateButtonState()
let error = RDNAIDVService.shared.checkIDVUserBiometricTemplateStatus()
if error.longErrorCode != 0 {
print("ServerBiometricAuthenticationViewController - CheckIDVUserBiometricTemplateStatus sync error:")
print(" Error: \(error.errorString)")
print(" Code: \(error.longErrorCode)")
isCheckingStatus = false
updateTemplateStatus(checked: true, exists: false, statusCode: 0, statusMessage: "Error: \(error.errorString)")
updateButtonState()
showAlert(title: "Template Status Check Error", message: "Failed to check template status: \(error.errorString)")
} else {
print("ServerBiometricAuthenticationViewController - CheckIDVUserBiometricTemplateStatus sync success")
print(" Awaiting onIDVCheckUserBiometricTemplateStatus callback")
// The callback will update UI
}
}
private func handleCheckUserBiometricTemplateStatus(userID: String, idvResponse: String, status: RDNARequestStatus, error: RDNAError) {
print("ServerBiometricAuthenticationViewController - Check user biometric template status callback received")
print(" User ID: \(userID)")
print(" Status Code: \(status.statusCode)")
print(" Status Message: \(status.statusMessage ?? "nil")")
print(" Error Code: \(error.longErrorCode)")
isCheckingStatus = false
// First check: error code
if error.longErrorCode != 0 {
print(" Error checking template status: \(error.errorString)")
updateTemplateStatus(checked: true, exists: false, statusCode: Int(status.statusCode), statusMessage: "Error: \(error.errorString)")
updateButtonState()
return
}
// Second check: status code
if status.statusCode == 100 {
// Template present
print(" Template exists")
updateTemplateStatus(checked: true, exists: true, statusCode: Int(status.statusCode), statusMessage: status.statusMessage)
} else if status.statusCode == 600 {
// Template does not exist
print(" Template does not exist")
updateTemplateStatus(checked: true, exists: false, statusCode: Int(status.statusCode), statusMessage: status.statusMessage)
} else {
// Other status codes (error)
print(" Status error: \(status.statusCode)")
updateTemplateStatus(checked: true, exists: false, statusCode: Int(status.statusCode), statusMessage: status.statusMessage)
}
updateButtonState()
}
private func updateTemplateStatus(checked: Bool, exists: Bool, statusCode: Int, statusMessage: String) {
templateStatus.checked = checked
templateStatus.exists = exists
templateStatus.statusCode = statusCode
templateStatus.statusMessage = statusMessage
// Update UI
templateStatusCard.isHidden = false
if exists {
templateStatusTitleLabel.text = "✅ Biometric Template Status"
templateStatusTextLabel.text = "Your biometric template is stored on the server. You can proceed with authentication."
styleCard(templateStatusCard, borderColor: UIColor(hex: "#27ae60"))
} else {
templateStatusTitleLabel.text = "⚠️ Biometric Template Status"
templateStatusTextLabel.text = "No biometric template found on the server. Please complete biometric opt-in first."
styleCard(templateStatusCard, borderColor: UIColor(hex: "#e74c3c"))
}
templateStatusCodeLabel.text = "Status: \(statusMessage) (Code: \(statusCode))"
}
// MARK: - Authentication
@IBAction func startButtonTapped(_ sender: UIButton) {
print("ServerBiometricAuthenticationViewController - Start button tapped")
isAuthenticating = true
updateButtonState()
let reason = "Server Biometric Authentication"
let error = RDNAIDVService.shared.initiateIDVServerBiometricAuthentication(reason: reason)
if error.longErrorCode != 0 {
print("ServerBiometricAuthenticationViewController - InitiateIDVServerBiometricAuthentication sync error:")
print(" Error: \(error.errorString)")
print(" Code: \(error.longErrorCode)")
isAuthenticating = false
updateButtonState()
showAlert(title: "Authentication Error", message: "Failed to initiate authentication: \(error.errorString)")
} else {
print("ServerBiometricAuthenticationViewController - InitiateIDVServerBiometricAuthentication sync success")
print(" Awaiting onIDVServerBiometricAuthenticationResult callback")
// The callback will update UI
}
}
private func handleServerBiometricAuthenticationResult(userID: String, idvResponse: String, status: RDNARequestStatus, error: RDNAError) {
print("ServerBiometricAuthenticationViewController - Server biometric authentication result callback received")
print(" User ID: \(userID)")
print(" Status Code: \(status.statusCode)")
print(" Status Message: \(status.statusMessage)")
print(" Error Code: \(error.longErrorCode)")
// Reset button state
isAuthenticating = false
updateButtonState()
// Dismiss SDK selfie screen first, then show alert
print("ServerBiometricAuthenticationViewController - Dismissing selfie screen")
dismissSelfieScreenAndShowResult(idvResponse: idvResponse, status: status, error: error)
}
private func dismissSelfieScreenAndShowResult(idvResponse: String, status: RDNARequestStatus, error: RDNAError) {
// Check if there are other view controllers on the navigation stack above us
if let navController = navigationController,
let selfIndex = navController.viewControllers.firstIndex(of: self),
selfIndex < navController.viewControllers.count - 1 {
// Pop back to this view controller
print("ServerBiometricAuthenticationViewController - Popping back to self")
navController.popToViewController(self, animated: true)
// Wait for pop animation to complete
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
print("ServerBiometricAuthenticationViewController - Pop completed, showing result")
self?.processAuthenticationResult(idvResponse: idvResponse, status: status, error: error)
}
} else {
print("ServerBiometricAuthenticationViewController - Already on top, showing result directly")
processAuthenticationResult(idvResponse: idvResponse, status: status, error: error)
}
}
private func processAuthenticationResult(idvResponse: String, status: RDNARequestStatus, error: RDNAError) {
print("ServerBiometricAuthenticationViewController - Processing authentication result")
// First check: error code
if error.longErrorCode != 0 {
print(" Error in authentication: \(error.errorString)")
showAlert(
title: "Authentication Error",
message: "\(error.errorString)\n\nError Code: \(error.longErrorCode)"
)
return
}
// Second check: status code (100 or 0 = success for IDV server biometric auth)
if status.statusCode != 100 && status.statusCode != 0 {
print(" Status error: \(status.statusCode)")
showAlert(
title: "Authentication Failed",
message: "Status: \(status.statusMessage)\nStatus Code: \(status.statusCode)"
)
return
}
// Success - parse idvResponse to check audit status
print(" ✅ Authentication completed successfully, parsing result")
parseAndShowAuthenticationResult(idvResponse: idvResponse)
}
private func parseAndShowAuthenticationResult(idvResponse: String) {
print("ServerBiometricAuthenticationViewController - Parsing authentication result")
print(" IDV Response: \(idvResponse)")
guard let data = idvResponse.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
print(" ⚠️ Failed to parse JSON")
showAlert(
title: "Authentication Complete",
message: "Authentication completed successfully"
)
return
}
// Parse biometric result
let biometricResult = json["biometric_result"] as? [String: Any]
let displayText = biometricResult?["display_text"] as? String ?? "Unknown"
// Parse audit info
let auditInfo = json["idv_audit_info"] as? [String: Any]
let orchestrationUseCase = auditInfo?["orchestration_use_case"] as? String ?? "N/A"
let transactionId = auditInfo?["idv_audit_transaction_id"] as? String ?? "N/A"
let attemptsLeft = json["attempts_left"] as? Int ?? 0
// Determine success based on biometric result display text
let isSuccess = displayText == "MATCHED"
if isSuccess {
showAlert(
title: "✅ Authentication Success",
message: """
Biometric Result: \(displayText)
Use Case: \(orchestrationUseCase)
Transaction ID: \(transactionId)
Attempts Left: \(attemptsLeft)
""",
actions: [
UIAlertAction(title: "Back to Dashboard", style: .default) { [weak self] _ in
self?.navigateToDashboard()
}
]
)
} else {
showAlert(
title: "❌ Authentication Failed",
message: """
Biometric Result: \(displayText)
Use Case: \(orchestrationUseCase)
Transaction ID: \(transactionId)
Attempts Left: \(attemptsLeft)
"""
)
}
}
// MARK: - Button State
private func updateButtonState() {
if isCheckingStatus {
startButton.setTitle("Checking Template Status...", for: .normal)
startButton.isEnabled = false
startButton.backgroundColor = UIColor(hex: "#95a5a6") ?? .gray
disabledNoteLabel.isHidden = true
} else if isAuthenticating {
startButton.setTitle("Authenticating...", for: .normal)
startButton.isEnabled = false
startButton.backgroundColor = UIColor(hex: "#95a5a6") ?? .gray
disabledNoteLabel.isHidden = true
} else if !templateStatus.exists && templateStatus.checked {
startButton.setTitle("Start Authentication", for: .normal)
startButton.isEnabled = false
startButton.backgroundColor = UIColor(hex: "#95a5a6") ?? .gray
disabledNoteLabel.isHidden = false
} else {
startButton.setTitle("Start Authentication", for: .normal)
startButton.isEnabled = templateStatus.exists
startButton.backgroundColor = templateStatus.exists ? (UIColor(hex: "#3498db") ?? .blue) : (UIColor(hex: "#95a5a6") ?? .gray)
disabledNoteLabel.isHidden = !templateStatus.checked || templateStatus.exists
}
}

When the template exists (status code 100), a green banner displays "✅ Your biometric template is stored on the server" and the "Start Authentication" button is enabled (blue).

When no template is found (status code 600), a red banner displays "⚠️ No biometric template found on the server" with help text, and the "Start Authentication" button is disabled (grey).
Update AppIDVCoordinator.swift:
// Sources/Tutorial/Navigation/AppIDVCoordinator.swift
class AppIDVCoordinator {
static let shared = AppIDVCoordinator()
private weak var navigationController: UINavigationController?
private let storyboard = UIStoryboard(name: "Main", bundle: nil)
private init() {}
func configure(with navigationController: UINavigationController) {
self.navigationController = navigationController
}
func showServerBiometricAuthentication(animated: Bool = true) {
guard let vc = storyboard.instantiateViewController(
withIdentifier: "ServerBiometricAuthenticationViewController"
) as? ServerBiometricAuthenticationViewController else {
print("IDVCoordinator - Failed to instantiate ServerBiometricAuthenticationViewController")
return
}
DispatchQueue.main.async { [weak self] in
self?.navigationController?.pushViewController(vc, animated: animated)
print("IDVCoordinator - Navigated to ServerBiometricAuthentication")
}
}
}
Update MenuViewController.swift:
// Sources/Tutorial/Screens/MenuViewController.swift
enum MenuOption: CaseIterable {
case dashboard
case serverBiometricAuthentication
case deviceManagement
// ... other options
var title: String {
switch self {
case .serverBiometricAuthentication:
return "🔐 Biometric Authentication"
// ... other cases
}
}
}
// In menu button handler:
case .serverBiometricAuthentication:
print("MenuViewController - Navigating to server biometric authentication")
AppIDVCoordinator.shared.showServerBiometricAuthentication()
dismiss(animated: true)
Add the view controller to Main.storyboard:
<!-- Storyboard ID: ServerBiometricAuthenticationViewController -->
<!-- Class: ServerBiometricAuthenticationViewController -->
<!-- Module: relidcodelab -->
Test the complete server biometric authentication flow:
1. Launch the app
2. Complete user login (activated customer)
3. Navigate to Dashboard
4. Tap menu button (hamburger icon)
5. Select "🔐 Biometric Authentication"
1. Screen loads and automatically checks template status
2. Observe loading state: "Checking template status..."
3. Template status displayed:
- ✅ If template exists (status code 100):
- Green banner with "Your biometric template is stored on the server"
- "Start Authentication" button enabled (blue)
- ⚠️ If no template (status code 600):
- Red banner with "No biometric template found on the server"
- "Start Authentication" button disabled (grey)
- Help text: "Please complete biometric opt-in first"
1. Tap "Start Authentication" button
2. Button shows loading: "Authenticating..."
3. SDK initiates workflow 8 (selfie capture)
4. Selfie confirmation screen appears (reused from IDV MFA flow)
1. Tap "Start Selfie Capture" button
→ Native camera opens with Aware FaceCapture
→ Liveness detection guides user
→ User captures selfie
→ Camera closes
2. OR Tap Close button (X)
→ Error 240 triggered (user cancelled)
→ Result callback fires immediately
1. Navigation back to ServerBiometricAuthentication screen
2. 500ms delay
3. Result alert appears:
Success Alert (biometric result = MATCHED):
✅ Authentication Success
- Biometric Result: MATCHED
- Use Case: RDNA_IDV_POSTLOGIN_IDV_SELFIE_BIOMETRIC
- Transaction ID: [unique ID]
- Attempts Left: [number]
- Button: "Back to Dashboard"
Failure Alert (biometric result != MATCHED):
❌ Authentication Failed
- Biometric Result: NOT_MATCHED
- Use Case: RDNA_IDV_POSTLOGIN_IDV_SELFIE_BIOMETRIC
- Transaction ID: [unique ID]
- Attempts Left: [number]
- Button: "OK"
Cancellation (error 240):
Authentication Error
- User Cancelled
- Error Code: 240
- Button: "OK"
✅ Template check returns status code 100
✅ Button enabled
✅ User taps "Start Authentication"
✅ Selfie capture completes
✅ Authentication succeeds (biometric result = MATCHED)
✅ Success alert shown
✅ User taps "Back to Dashboard"
✅ Navigates to Dashboard
⚠️ Template check returns status code 600
⚠️ Button disabled (grey color)
⚠️ Red banner: "No biometric template found"
⚠️ Help text: "Please complete biometric opt-in first"
⚠️ User cannot proceed with authentication
✅ Template check succeeds (status code 100)
✅ User taps "Start Authentication"
✅ Selfie confirmation screen appears
❌ User taps Close button (X)
❌ Error 240 triggered
❌ Navigate back to authentication screen
❌ Error alert: "User Cancelled (Error Code: 240)"
✅ User taps "OK"
✅ Button re-enabled, can retry
✅ Template check succeeds
✅ User completes selfie capture
❌ Server determines face doesn't match
❌ Biometric result = NOT_MATCHED
❌ Navigate back to authentication screen
❌ Failure alert with audit data
✅ User can retry authentication
✅ User completes authentication (success or failure)
✅ User navigates to Dashboard
✅ User returns to Biometric Authentication from menu
✅ viewWillAppear() triggers → Re-registers handlers
✅ Template status check runs automatically
✅ UI state reset (isAuthenticating = false)
✅ Template status displayed
✅ User can perform another authentication
Use this checklist to verify your implementation:
You've successfully implemented the REL-ID IDV Post-Login Server Biometric Authentication Flow in your iOS application!
✅ Template Status Validation: Implemented automatic biometric template checking with status codes 100/600
✅ Server Biometric Authentication: Built complete authentication workflow with workflow 8 and challenge mode 25
✅ View Lifecycle Pattern: Mastered persistent callback handlers during native UI workflows
✅ Navigate-Back-Before-Alert Pattern: Implemented proper navigation for result display
✅ Audit Data Display: Parsed and displayed comprehensive authentication audit information
✅ Error Handling: Gracefully handled error 240 (user cancellation) and authentication failures
✅ Production-Ready Implementation: Built robust, user-friendly biometric verification experience
biometric_result.display_text for MATCHED/NOT_MATCHED validationNow that you've mastered server biometric authentication, consider:
🔐 Thank you for completing the REL-ID IDV Post-Login Server Biometric Authentication Flow codelab!
You're now equipped to implement production-ready server-side biometric verification for activated customers with stored templates, providing secure and convenient identity verification with liveness detection and comprehensive audit trails.