Multi-Device Management
Overview
Wafra supports secure access across multiple devices through a sophisticated approval system that maintains security while providing user convenience. Users can register passkeys on multiple devices (phones, tablets, computers) with a cryptographic approval workflow.
Device Registration Flow
First Device (During Onboarding)
Automatic Approval:
- First passkey is automatically approved during user onboarding
- Becomes the primary authentication method
- Enables wallet operations immediately
- No approval workflow required
Implementation: apps/server/src/api/controllers/passkey.ts:106-137
// Check if this is the first passkey for the user
const existingPasskeys = await prisma.passkey.findMany({
where: { userId: user.id },
});
const isFirstPasskey = existingPasskeys.length === 0;
const passkey = await prisma.passkey.create({
data: {
userId: user.id,
credentialId: credential.rawId,
publicKey: credential.response.publicKey!,
counter,
deviceType: deviceType || "unknown",
deviceName: deviceName || "Unknown Device",
approved: isFirstPasskey, // First passkey is automatically approved
},
});Additional Devices
Approval Required:
- New devices require approval from existing approved device
- 24-hour approval window with automatic expiration
- Real-time notifications via Socket.IO
- Cryptographic proof of ownership required
Implementation: apps/app/app/auth/device-setup.tsx
const handleAddPasskey = async () => {
try {
await registerPasskey({
deviceName: deviceName.trim(),
deviceType: Platform.OS,
});
Alert.alert(
"Device Added Successfully",
`Your device "${deviceName}" has been added but needs approval from another device.`,
[
{
text: "Check Status",
onPress: () => router.push("/auth/approval-status"),
},
]
);
} catch (error: any) {
setError(error.message || "Failed to set up device");
}
};Device Approval Workflow
Pending Request Notification
Header Badge: apps/app/components/dashboard/Header.tsx
The header displays a notification badge when there are pending device approval requests:
const { showApprovalModal, pendingRequestsCount } = useDeviceApproval();
{
pendingRequestsCount > 0 && (
<View className="absolute -top-1 -right-1 bg-red-500 rounded-full w-4 h-4">
<Text className="text-white text-xs font-bold">
{pendingRequestsCount}
</Text>
</View>
);
}Device Management Interface
Device List: apps/app/app/devices/index.tsx
Users can view all their registered devices and pending requests:
const { data: passkeys } = useQuery({
queryKey: ["passkeys"],
queryFn: () => exec("/passkeys"),
});
const { data: pendingRequests } = useQuery({
queryKey: ["passkeys", "pending"],
queryFn: () => exec("/passkeys/approval/pending"),
});Approval Process
Approval Interface: apps/app/app/devices/[id].tsx
Users can approve or reject pending device requests:
const handleApprove = async () => {
try {
await exec("/passkeys/approval/approve", {
method: "POST",
body: JSON.stringify({
requestId: request.id,
approved: true,
}),
});
Alert.alert(
"Device Approved",
"The device has been approved successfully."
);
router.back();
} catch (error: any) {
setError(error.message || "Failed to approve device");
}
};
const handleReject = async () => {
try {
await exec("/passkeys/approval/approve", {
method: "POST",
body: JSON.stringify({
requestId: request.id,
approved: false,
}),
});
Alert.alert("Device Rejected", "The device has been rejected.");
router.back();
} catch (error: any) {
setError(error.message || "Failed to reject device");
}
};Security Controls
Device Removal Protection
Security Checks: apps/server/src/api/controllers/passkey.ts:200-250
Multiple security controls prevent unauthorized device removal:
// Session age validation (≤5 minutes)
const sessionAge = Date.now() - ctx.session.iat * 1000;
const maxSessionAge = 5 * 60 * 1000; // 5 minutes
if (sessionAge > maxSessionAge) {
throw new AppError(400, "Session too old for device removal");
}
// Cannot remove current session's passkey
if (passkey.id === ctx.session.passkeyId) {
throw new AppError(400, "Cannot remove current device");
}
// Cannot remove last approved device
const approvedPasskeys = await prisma.passkey.findMany({
where: { userId: user.id, approved: true },
});
if (approvedPasskeys.length <= 1) {
throw new AppError(400, "Cannot remove last approved device");
}Approval Expiration
Time Limits: apps/server/src/api/controllers/passkey.ts:150-180
Device approval requests automatically expire:
// Create approval request with 24-hour expiration
await prisma.deviceApprovalRequest.create({
data: {
userId: user.id,
requestingPasskeyId: passkey.id,
deviceName: deviceName || "Unknown Device",
deviceType: deviceType || "unknown",
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
},
});Real-Time Features
Socket.IO Integration
Event Handling: apps/server/src/services/socket/socket.service.ts
Real-time notifications for device management:
// Device approval request notification
io.to(`user:${userId}`).emit("deviceApprovalRequest", {
type: "deviceApprovalRequest",
data: {
requestId: request.id,
deviceName: request.deviceName,
deviceType: request.deviceType,
createdAt: request.createdAt,
},
});
// Device approval/rejection notification
io.to(`user:${userId}`).emit("deviceApprovalResponse", {
type: "deviceApprovalResponse",
data: {
requestId: request.id,
approved: request.approved,
deviceName: request.deviceName,
},
});React Query Integration
Automatic Invalidation: apps/app/hooks/useDeviceApproval.tsx
Queries are automatically invalidated when device status changes:
const queryClient = useQueryClient();
// Invalidate relevant queries when device status changes
useEffect(() => {
if (deviceApprovalResponse) {
queryClient.invalidateQueries(["passkeys"]);
queryClient.invalidateQueries(["passkeys", "pending"]);
}
}, [deviceApprovalResponse, queryClient]);Database Schema
Passkey Model
Schema: apps/server/prisma/schema.prisma
model Passkey {
id String @id @default(cuid())
userId String
credentialId String @unique
publicKey String
counter Int @default(0)
deviceType String?
deviceName String?
approved Boolean @default(false)
createdAt DateTime @default(now())
lastUsedAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
deviceApprovalRequests DeviceApprovalRequest[] @relation("RequestingPasskey")
}Device Approval Request Model
model DeviceApprovalRequest {
id String @id @default(cuid())
userId String
requestingPasskeyId String
deviceName String
deviceType String
approved Boolean?
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
requestingPasskey Passkey @relation("RequestingPasskey", fields: [requestingPasskeyId], references: [id], onDelete: Cascade)
}API Endpoints
Passkey Management
tRPC Procedures:
passkey.generateRegistrationChallenge: Start passkey registrationpasskey.verifyRegistration: Complete passkey registrationpasskey.generateAuthenticationChallenge: Start passkey authenticationpasskey.verifyAuthentication: Complete passkey authenticationpasskey.listPasskeys: Get user’s devicespasskey.getPendingApprovals: Get pending approval requestspasskey.approveRequest: Approve/reject device requestspasskey.removePasskey: Remove device from account
REST Endpoints (Legacy)
GET /passkeys- List user’s passkeysPOST /passkeys/register- Register new passkeyPOST /passkeys/authenticate- Authenticate with passkeyPOST /passkeys/remove- Secure device removal with authentication checksGET /passkeys/approval/pending- Get pending device approval requestsPOST /passkeys/approval/approve- Approve or reject device requestsGET /passkeys/approval/status- Check approval status for requesting device
User Experience
Device Setup Flow
- User initiates device setup from device management screen
- System generates registration challenge with device metadata
- User registers passkey using device biometrics
- System creates approval request (if not first device)
- User receives notification of pending approval
- Existing device approves/rejects the new device
- User receives confirmation of approval status
Device Management Flow
- User views device list with approval status
- User can approve/reject pending requests
- User can remove devices with security checks
- Real-time updates via Socket.IO notifications
- Automatic query invalidation via React Query
Security Features
Multi-Layer Protection
- Cryptographic Proof: Each device must prove ownership through WebAuthn
- Approval Workflow: New devices require explicit approval
- Session Validation: Recent session required for sensitive operations
- Expiration Handling: Automatic cleanup of expired requests
- Last Device Protection: Cannot remove the last approved device
Audit Trail
- All device operations are logged with timestamps
- Approval/rejection actions are tracked
- Device metadata is stored for security analysis
- Session information is maintained for compliance
This multi-device management system provides enterprise-grade security while maintaining the simplicity users expect from modern financial applications.