Documentation Index
Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
Use this file to discover all available pages before exploring further.
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 15.5+ (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:
- Display all recovery codes clearly
- Prompt the user to save them securely
- Call
acknowledgeRecoveryCodes() once the user confirms they’ve saved them
- 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:
- Passkey Setup - Configure passkeys for 1FA sign-in or MFA
- Wallet Operations - Perform wallet operations
- 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.