10 KiB
Device Management Features
Overview
The auth service now supports proper multi-device login with device management capabilities. One phone number = One account, but that account can be logged in from multiple devices simultaneously.
Key Features
✅ Multi-Device Support
- Same phone number can log in from multiple devices
- Each device gets its own refresh token
- Devices can be active simultaneously
- Independent sessions per device
✅ Device Tracking
- All login attempts are logged to
auth_audittable - New device detection flags (
is_new_device,is_new_account) - Device metadata tracking (platform, model, OS version, etc.)
✅ Device Management
- List all active devices
- Revoke/logout specific devices
- Logout all other devices (keep current)
Updated Endpoints
1. Verify OTP (Enhanced Response)
Endpoint: POST /auth/verify-otp
Response now includes:
{
"user": { ... },
"access_token": "...",
"refresh_token": "...",
"needs_profile": true,
"is_new_device": false, // ← NEW: Is this a new device?
"is_new_account": false, // ← NEW: Is this a new account?
"active_devices_count": 2 // ← NEW: How many devices are active?
}
Use Cases:
is_new_device: true→ Show security notification to useris_new_account: true→ Welcome new user flowactive_devices_count→ Display in settings/profile
2. Get User Info
Endpoint: GET /users/me
Headers:
Authorization: Bearer <access_token>
Response:
{
"id": "uuid",
"phone_number": "+919876543210",
"name": "John Doe",
"role": "user",
"user_type": "seller",
"created_at": "2024-01-01T00:00:00Z",
"last_login_at": "2024-01-15T10:30:00Z",
"active_devices_count": 2
}
3. List Active Devices
Endpoint: GET /users/me/devices
Headers:
Authorization: Bearer <access_token>
Response:
{
"devices": [
{
"device_identifier": "android-device-123",
"device_platform": "android",
"device_model": "Samsung Galaxy S21",
"os_version": "Android 14",
"app_version": "1.0.0",
"language_code": "en-IN",
"timezone": "Asia/Kolkata",
"first_seen_at": "2024-01-10T08:00:00Z",
"last_seen_at": "2024-01-15T10:30:00Z",
"is_active": true
},
{
"device_identifier": "iphone-device-456",
"device_platform": "ios",
"device_model": "iPhone 13",
"os_version": "iOS 17.2",
"app_version": "1.0.0",
"language_code": "en-IN",
"timezone": "Asia/Kolkata",
"first_seen_at": "2024-01-12T14:20:00Z",
"last_seen_at": "2024-01-15T09:15:00Z",
"is_active": true
}
]
}
4. Revoke Specific Device
Endpoint: DELETE /users/me/devices/:device_id
Headers:
Authorization: Bearer <access_token>
Response:
{
"ok": true,
"message": "Device logged out successfully"
}
What it does:
- Marks device as inactive in
user_devicestable - Revokes all refresh tokens for that device
- Logs the action in
auth_audittable
Kotlin Example:
suspend fun revokeDevice(deviceId: String, accessToken: String): Result<Unit> {
val response = apiClient.delete("/users/me/devices/$deviceId") {
header("Authorization", "Bearer $accessToken")
}
return if (response.status.isSuccess()) {
Result.success(Unit)
} else {
Result.failure(Exception("Failed to revoke device"))
}
}
5. Logout All Other Devices
Endpoint: POST /users/me/logout-all-other-devices
Headers:
Authorization: Bearer <access_token>
X-Device-Id: <current_device_id> // ← Required header
OR Request Body:
{
"current_device_id": "android-device-123"
}
Response:
{
"ok": true,
"message": "Logged out 2 device(s)",
"revoked_devices_count": 2
}
What it does:
- Keeps current device active
- Logs out all other devices
- Revokes refresh tokens for all other devices
Kotlin Example:
suspend fun logoutAllOtherDevices(
currentDeviceId: String,
accessToken: String
): Result<LogoutAllResponse> {
val response = apiClient.post("/users/me/logout-all-other-devices") {
header("Authorization", "Bearer $accessToken")
header("X-Device-Id", currentDeviceId)
contentType(ContentType.Application.Json)
setBody(JsonObject(mapOf(
"current_device_id" to JsonPrimitive(currentDeviceId)
)))
}
return Result.success(response.body())
}
Authentication Flow Example
Scenario: User logs in from new phone
-
Request OTP (same phone number)
POST /auth/request-otp { "phone_number": "+919876543210" } -
Verify OTP (from new device)
POST /auth/verify-otp { "phone_number": "+919876543210", "code": "123456", "device_id": "new-phone-device-id", "device_info": { "platform": "android", ... } } -
Response:
{ "user": { ... }, "access_token": "...", "refresh_token": "...", "is_new_device": true, // ← This is a new device "is_new_account": false, // ← But existing account "active_devices_count": 2 // ← Now 2 devices active } -
Mobile App Action:
- Show notification: "New device logged in: Android Phone"
- Display in security settings: "Active Devices: 2"
- Allow user to revoke old device if needed
Security Features
✅ New Device Detection
- Automatically detected on login
- Logged in
auth_audittable - Flag returned in response for app to show alert
✅ Device Activity Tracking
last_seen_atupdated on token refresh- Tracks when device was last active
- Helps identify abandoned/inactive devices
✅ Audit Logging
All authentication events logged:
- Login attempts (success/failure)
- Device revocations
- Logout actions
- Token refreshes
Query audit logs:
SELECT * FROM auth_audit
WHERE user_id = 'user-uuid'
ORDER BY created_at DESC;
Mobile App Implementation
Show Active Devices Screen
class DeviceManagementActivity : AppCompatActivity() {
private val authManager = AuthManager(...)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val devices = authManager.getActiveDevices()
devices.forEach { device ->
// Display device info
// Show "Revoke" button
}
}
}
private fun revokeDevice(deviceId: String) {
lifecycleScope.launch {
authManager.revokeDevice(deviceId)
.onSuccess {
// Refresh device list
// Show toast: "Device logged out"
}
.onFailure { showError("Failed to revoke device") }
}
}
}
Handle New Device Login
private fun handleLoginResponse(response: VerifyOtpResponse) {
if (response.is_new_device && !response.is_new_account) {
// Show security alert
showDialog(
title = "New Device Detected",
message = "You've logged in from a new device. " +
"If this wasn't you, please change your password.",
positiveButton = "OK"
)
}
// Save tokens
tokenManager.saveTokens(response.access_token, response.refresh_token)
// Navigate to home/profile
}
Database Schema
user_devices Table
CREATE TABLE user_devices (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
device_identifier TEXT,
device_platform TEXT NOT NULL,
device_model TEXT,
os_version TEXT,
app_version TEXT,
language_code TEXT,
timezone TEXT,
first_seen_at TIMESTAMPTZ NOT NULL,
last_seen_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (user_id, device_identifier)
);
auth_audit Table
CREATE TABLE auth_audit (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL, -- 'login', 'device_revoked', etc.
status VARCHAR(50) NOT NULL, -- 'success', 'failed'
device_id VARCHAR(255),
ip_address VARCHAR(45),
user_agent TEXT,
meta JSONB,
created_at TIMESTAMPTZ NOT NULL
);
Important Notes
-
Phone Number = Account Ownership
- One phone number = One account
- If someone else uses your phone number, they access your account
- Always protect your phone number/SIM card
-
Multiple Devices = Same Account
- All devices access the same user account
- Data is shared across devices
- Logout on one device doesn't affect others
-
Device ID Must Be Consistent
- Use same
device_idfor same physical device - Don't randomly generate new IDs
- Use Android ID, Installation ID, or Firebase Installation ID
- Use same
-
Token Rotation
- Refresh tokens rotate on each refresh
- Always save the new
refresh_token - Old tokens become invalid
-
Device Revocation
- Revoking a device logs it out immediately
- Refresh tokens for that device are revoked
- User must re-login on that device
Testing
Test Multi-Device Login
# Device 1
curl -X POST http://localhost:3000/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+919876543210",
"code": "123456",
"device_id": "device-1"
}'
# Device 2 (same phone, different device)
curl -X POST http://localhost:3000/auth/verify-otp \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+919876543210",
"code": "123456",
"device_id": "device-2"
}'
# Check active devices
curl -X GET http://localhost:3000/users/me/devices \
-H "Authorization: Bearer <access_token>"
Both devices should be logged in and visible in the devices list.