auth/Documentaion/implementation/DEVICE_MANAGEMENT.md

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_audit table
  • 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 user
  • is_new_account: true → Welcome new user flow
  • active_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_devices table
  • Revokes all refresh tokens for that device
  • Logs the action in auth_audit table

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

  1. Request OTP (same phone number)

    POST /auth/request-otp
    { "phone_number": "+919876543210" }
    
  2. Verify OTP (from new device)

    POST /auth/verify-otp
    {
      "phone_number": "+919876543210",
      "code": "123456",
      "device_id": "new-phone-device-id",
      "device_info": { "platform": "android", ... }
    }
    
  3. 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
    }
    
  4. 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_audit table
  • Flag returned in response for app to show alert

Device Activity Tracking

  • last_seen_at updated 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

  1. 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
  2. Multiple Devices = Same Account

    • All devices access the same user account
    • Data is shared across devices
    • Logout on one device doesn't affect others
  3. Device ID Must Be Consistent

    • Use same device_id for same physical device
    • Don't randomly generate new IDs
    • Use Android ID, Installation ID, or Firebase Installation ID
  4. Token Rotation

    • Refresh tokens rotate on each refresh
    • Always save the new refresh_token
    • Old tokens become invalid
  5. 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.