Skip to main content

Overview

Multi-Factor Authentication (MFA) adds an additional security layer to your app by requiring users to provide two forms of verification. The Dynamic Swift SDK supports:
  • TOTP (Time-based One-Time Password) - Codes from authenticator apps like Google Authenticator or Authy
  • Passkey MFA - Biometric authentication as a second factor
  • Recovery Codes - Backup codes for account recovery
MFA must be enabled in your Dynamic Dashboard under Security settings before you can use it in your app.

Prerequisites

  • Dynamic SDK initialized (see Installation Guide)
  • User must be authenticated
  • MFA enabled in your Dynamic dashboard
  • iOS 13.0+ (iOS 16.0+ for passkey support)

MFA Device Management

Get User’s MFA Devices

Fetch all MFA devices registered for the current user:
import DynamicSDKSwift

let sdk = DynamicSDK.instance()

do {
    let devices = try await sdk.mfa.getUserDevices()
    print("Found \(devices.count) MFA device(s)")

    for device in devices {
        print("📱 Device ID: \(device.id ?? "N/A")")
        print("   Type: \(device.type?.rawValue ?? "Unknown")")
        print("   Created: \(device.createdAt ?? "Unknown")")
    }
} catch {
    print("❌ Failed to fetch MFA devices: \(error)")
}

Add a New TOTP Device

Register a new TOTP device (authenticator app):
let sdk = DynamicSDK.instance()

do {
    let device = try await sdk.mfa.addDevice(type: "totp")

    print("✅ MFA device added")
    print("Secret: \(device.secret)")

    // The device contains a secret that can be displayed as a QR code
    // for users to scan with their authenticator app

} catch {
    print("❌ Failed to add MFA device: \(error)")
}

Verify a New Device

After adding a device, verify it with a TOTP code from your authenticator app:
let sdk = DynamicSDK.instance()
let totpCode = "123456" // 6-digit code from authenticator app

do {
    try await sdk.mfa.verifyDevice(totpCode, type: "totp")
    print("✅ Device verified successfully")
} catch {
    print("❌ Device verification failed: \(error)")
}

Authenticate with an Existing Device

Authenticate with an existing MFA device to get an MFA token:
let sdk = DynamicSDK.instance()
let deviceId = "device_id"
let totpCode = "123456" // 6-digit code from authenticator app

do {
    let mfaToken = try await sdk.mfa.authenticateDevice(
        params: MfaAuthenticateDevice(
            code: totpCode,
            deviceId: deviceId,
            createMfaToken: MfaCreateToken(singleUse: false)
        )
    )

    print("✅ Device authenticated successfully")
    print("MFA Token: \(mfaToken ?? "nil")")

} catch {
    print("❌ Device authentication failed: \(error)")
}

Delete an MFA Device

Remove an MFA device from the user’s account:
let sdk = DynamicSDK.instance()
let deviceId = "device_id_to_delete"
let totpCode = "123456"

do {
    // Step 1: Authenticate to get MFA token
    let mfaToken = try await sdk.mfa.authenticateDevice(
        params: MfaAuthenticateDevice(
            code: totpCode,
            deviceId: deviceId,
            createMfaToken: MfaCreateToken(singleUse: true)
        )
    )

    guard let token = mfaToken else {
        print("❌ Failed to get MFA token")
        return
    }

    // Step 2: Delete the device
    try await sdk.mfa.deleteUserDevice(
        deviceId: deviceId,
        mfaAuthToken: token
    )

    print("✅ MFA device deleted")
} catch {
    print("❌ Failed to delete device: \(error)")
}

Recovery Codes

Get Recovery Codes

After setting up MFA, users should save recovery codes for account recovery:
let sdk = DynamicSDK.instance()

do {
    let recoveryCodes = try await sdk.mfa.getRecoveryCodes(generateNewCodes: false)

    print("✅ Recovery codes retrieved:")
    for (index, code) in recoveryCodes.enumerated() {
        print("\(index + 1). \(code)")
    }

    // Display these codes to the user prominently
    // Encourage them to save codes in a secure location

} catch {
    print("❌ Failed to get recovery codes: \(error)")
}

Acknowledge Recovery Codes

After displaying recovery codes to the user, you should acknowledge that they’ve been shown:
let sdk = DynamicSDK.instance()

do {
    try await sdk.mfa.acknowledgeRecoveryCodes()
    print("✅ Recovery codes acknowledged")
} catch {
    print("❌ Failed to acknowledge recovery codes: \(error)")
}
Recovery Code Flow: When a user sets up MFA for the first time, they’ll receive recovery codes. The app should:
  1. Display all recovery codes clearly
  2. Prompt the user to save them securely
  3. Call acknowledgeRecoveryCodes() once the user confirms they’ve saved them
  4. Store the acknowledgment state to prevent showing codes repeatedly

Generate New Recovery Codes

If recovery codes are lost or compromised:
let sdk = DynamicSDK.instance()

do {
    let newCodes = try await sdk.mfa.getRecoveryCodes(generateNewCodes: true)

    print("✅ New recovery codes generated:")
    for (index, code) in newCodes.enumerated() {
        print("\(index + 1). \(code)")
    }

    // Important: Display these codes to the user and have them save securely

} catch {
    print("❌ Failed to regenerate recovery codes: \(error)")
}

Use Recovery Code for Authentication

If user loses their device, they can use a recovery code:
let sdk = DynamicSDK.instance()
let recoveryCode = "recovery_code_from_user"

do {
    try await sdk.mfa.authenticateRecoveryCode(code: recoveryCode)
    print("✅ Authenticated with recovery code")
} catch {
    print("❌ Recovery code authentication failed: \(error)")
}

Passkey MFA

Use passkeys as a second factor for enhanced security. Passkeys provide a seamless biometric authentication experience.
Configuration Required: Before using passkeys for MFA, you must configure associated domains and the SDK. See the Passkey Setup Guide for complete configuration instructions.
let sdk = DynamicSDK.instance()

func authenticateWithPasskeyMFA() async throws -> String? {
    do {
        // Authenticate using passkey as MFA
        let response = try await sdk.passkeys.authenticatePasskeyMFA(
            createMfaToken: MfaCreateToken(singleUse: true),
            relatedOriginRpId: nil
        )

        print("✅ Passkey authenticated for MFA")

        // The response contains a JWT token that can be used for authenticated operations
        return response.jwt

    } catch {
        print("❌ Passkey MFA authentication failed: \(error)")
        throw error
    }
}

Register Passkey as MFA Device

After initial authentication, users can register a passkey as an additional MFA method:
let sdk = DynamicSDK.instance()

func registerPasskeyForMFA() async throws {
    do {
        // Register a new passkey
        try await sdk.passkeys.registerPasskey()
        print("✅ Passkey registered successfully")

        // Now user can authenticate with this passkey as MFA
    } catch {
        print("❌ Failed to register passkey: \(error)")
        throw error
    }
}
Passkey Advantages: Passkeys offer several benefits over TOTP:
  • No need for a separate authenticator app
  • Biometric authentication (Face ID / Touch ID)
  • More secure against phishing
  • Faster authentication flow

Complete SwiftUI Example

Here’s a complete MFA management interface:
import SwiftUI
import DynamicSDKSwift

struct MfaManagementView: View {
    @StateObject private var vm = MfaManagementViewModel()

    var body: some View {
        List {
            // Add Device Section
            Section {
                Button {
                    vm.showingAddDevice = true
                } label: {
                    HStack {
                        Image(systemName: "plus.circle.fill")
                        Text("Add Authenticator App")
                    }
                }
            }

            // Existing Devices
            Section {
                if vm.devices.isEmpty {
                    Text("No MFA devices configured")
                        .foregroundColor(.secondary)
                } else {
                    ForEach(vm.devices, id: \.id) { device in
                        MfaDeviceRow(device: device) {
                            Task { await vm.deleteDevice(device.id) }
                        }
                    }
                }
            } header: {
                Text("Your MFA Devices")
            }

            // Recovery Codes
            Section {
                Button("View Recovery Codes") {
                    Task { await vm.showRecoveryCodes() }
                }

                Button("Regenerate Recovery Codes") {
                    Task { await vm.regenerateCodes() }
                }
            } header: {
                Text("Recovery Options")
            }
        }
        .navigationTitle("MFA Settings")
        .alert("Message", isPresented: .constant(vm.message.isEmpty == false)) {
            Button("OK") { vm.message = "" }
        } message: {
            Text(vm.message)
        }
        .task {
            await vm.loadDevices()
        }
    }
}

@MainActor
class MfaManagementViewModel: ObservableObject {
    @Published var devices: [MfaDevice] = []
    @Published var showingAddDevice = false
    @Published var message = ""

    private let sdk = DynamicSDK.instance()

    func loadDevices() async {
        do {
            devices = try await sdk.mfa.getUserDevices()
        } catch {
            message = "Failed to load devices: \(error.localizedDescription)"
        }
    }

    func showRecoveryCodes() async {
        do {
            let codes = try await sdk.mfa.getRecoveryCodes(generateNewCodes: false)
            message = "Recovery codes:\n" + codes.joined(separator: "\n")
        } catch {
            message = "Failed to get recovery codes: \(error.localizedDescription)"
        }
    }

    func regenerateCodes() async {
        do {
            let codes = try await sdk.mfa.getRecoveryCodes(generateNewCodes: true)
            message = "New recovery codes:\n" + codes.joined(separator: "\n")
        } catch {
            message = "Failed to regenerate recovery codes: \(error.localizedDescription)"
        }
    }

    func deleteDevice(_ deviceId: String?) async {
        guard let deviceId = deviceId else { return }
        // Would prompt for TOTP code first, then authenticate and delete
        message = "Enter TOTP code to delete device"
    }
}

struct MfaDeviceRow: View {
    let device: MfaDevice
    let onDelete: () -> Void

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("Authenticator App")
                    .font(.headline)

                Text("Added: \(formatDate(device.createdAt ?? ""))")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }

            Spacer()

            Button(role: .destructive, action: onDelete) {
                Image(systemName: "trash")
                    .foregroundColor(.red)
            }
            .buttonStyle(.borderless)
        }
    }

    func formatDate(_ dateString: String) -> String {
        let formatter = ISO8601DateFormatter()
        guard let date = formatter.date(from: dateString) else {
            return dateString
        }
        let displayFormatter = DateFormatter()
        displayFormatter.dateStyle = .medium
        return displayFormatter.string(from: date)
    }
}

Best Practices

1. User Experience

  • Clear instructions: Guide users through the setup process
  • QR code display: Make QR codes large and easy to scan
  • Recovery codes: Emphasize the importance of saving recovery codes
  • Multiple devices: Allow users to add multiple authenticator apps

2. Security

  • Require MFA for sensitive actions: Prompt for MFA when deleting accounts, changing settings, or performing financial transactions
  • Single-use tokens: Use singleUse: true for sensitive operations
  • Recovery code limits: Each recovery code can only be used once

3. Error Handling

let sdk = DynamicSDK.instance()

do {
    let devices = try await sdk.mfa.getUserDevices()
} catch {
    // Handle common MFA errors
    let errorDesc = error.localizedDescription.lowercased()

    if errorDesc.contains("invalid") {
        print("The code you entered is incorrect")
    } else if errorDesc.contains("expired") {
        print("Code expired, please try again")
    } else {
        print("MFA error: \(error.localizedDescription)")
    }
}

Troubleshooting

Code Not Working

  • Time sync: Ensure device time is synchronized (TOTP codes are time-based)
  • Code expired: TOTP codes expire every 30 seconds
  • Wrong device: Verify the correct authenticator app is being used

Can’t Delete Last Device

  • Users must keep at least one MFA method or disable MFA entirely
  • Consider implementing an admin reset flow

Recovery Codes Not Showing

  • Recovery codes are only available after a device is authenticated
  • User must authenticate with TOTP code first

Next Steps

Now that you have MFA set up, you can:
  1. Passkey Setup - Configure passkeys for 1FA sign-in or MFA
  2. Wallet Operations - Perform wallet operations
  3. Session Management - Manage authenticated sessions
The Swift ExampleApp includes a complete MFA implementation with device management, recovery codes, and passkey integration. See swift-sdk-and-sample-app/ExampleApp for the full code.