Skip to content

Conversation

@fadlikadn
Copy link

@fadlikadn fadlikadn commented Jan 12, 2026

Problem

When multiple users trust the same device/browser, the device trust cookie (hanko-device-token) gets overwritten with each new trust action. This causes previous users to lose their device trust status and be required to re-enter their TOTP code on every login. It's an improvement from initial PR about trust-device implementation (#1982)

Reproduction steps:

  1. User A logs in → sets up TOTP → trusts device → cookie set to tokenA
  2. User B logs in → sets up TOTP → trusts device → cookie overwritten to tokenB
  3. User A logs in again → cookie contains tokenB → MFA required (trust lost)

This is a common scenario in shared environments (family computers, shared workstations, demo environments, etc.) where multiple users need to authenticate from the same browser.

Solution

This PR introduces a composite cookie format that stores multiple user tokens in a single cookie, allowing each user to maintain their own device trust independently.

New cookie format:

<user_id_1>:<token_1>|<user_id_2>:<token_2>|...

Example:

a1b2c3d4-e5f6-7890-abcd-ef1234567890:dGVzdHRva2VuMQ==|e5f6g7h8-i9j0-1234-klmn-op5678901234:dGVzdHRva2VuMg==

Fixes the device trust overwrite issue for multi-user scenarios.

Implementation

Changes Overview

File Purpose
backend/config/config_mfa.go Added DeviceTrustMaxUsersPerDevice config field
backend/config/config_default.go Set default value to 20 users per device
backend/flow_api/services/device_trust.go Core parsing/serialization logic + updated trust check
backend/flow_api/flow/device_trust/hook_issue_trust_device_cookie.go Multi-user cookie management on trust action
deploy/docker-compose/config.yaml Added example MFA configuration

Key Design Decisions

1. Composite Token Format

Chose <user_id>:<token>|<user_id>:<token> format because:

  • Simple to parse with standard string operations
  • Graceful degradation (malformed entries are skipped, not fatal)
  • No external dependencies (vs JSON parsing)
  • Human-readable for debugging

2. Backward Compatibility

The implementation detects and handles legacy single-token cookies:

// Legacy format detection (no separators = single token)
if !strings.Contains(cookieValue, entrySeparator) && !strings.Contains(cookieValue, fieldSeparator) {
    return nil // Caller handles legacy migration
}

When a legacy cookie is detected:

  1. The token is validated against the database as before
  2. On the user's next trust action, the cookie is automatically migrated to the new format

3. Entry Management

  • Most recent first: New entries are prepended to maintain recency order
  • Duplicate prevention: Existing entries for the same user are removed before adding new one
  • Limit enforcement: Oldest entries (at the end) are trimmed when exceeding DeviceTrustMaxUsersPerDevice

4. Configuration

New config option with sensible default:

mfa:
  device_trust_max_users_per_device: 20  # ~100 bytes per user, 4KB cookie limit supports ~40 users

Code Flow

When checking device trust (CheckDeviceTrust):

1. Read cookie value
2. Parse into entries (or detect legacy format)
3. If legacy: validate single token against DB
4. If new format: find entry matching current user ID → validate token against DB
5. Return true if valid, unexpired token found for user

When issuing trust cookie (IssueTrustDeviceCookie):

1. Generate new token for current user
2. Store token in database
3. Read existing cookie entries
4. Filter out any existing entry for current user
5. Prepend new entry
6. Enforce max users limit (trim from end)
7. Serialize and set cookie

Tests

Manual Testing Steps

Setup

# Start Hanko with MFA enabled
docker compose -f deploy/docker-compose/quickstart.yaml -p "hanko-quickstart" up --build

Test Case 1: Multi-User Device Trust

  1. User A: Register/login → Set up TOTP → Enter code → Choose "Trust this device"
  2. User B: Register/login → Set up TOTP → Enter code → Choose "Trust this device"
  3. User A: Login again → Should skip MFA (device still trusted)
  4. User B: Login again → Should skip MFA (device still trusted)
  5. Verify cookie: Open DevTools → Application → Cookies → hanko-device-token should contain composite format with both user IDs

Test Case 2: Legacy Cookie Migration

  1. Manually set a legacy cookie: hanko-device-token=<single_token_value>
  2. Login as user with that token → Should validate successfully
  3. Trust device again → Cookie should migrate to new composite format

Test Case 3: Max Users Limit

  1. Set device_trust_max_users_per_device: 2 in config
  2. User A trusts device
  3. User B trusts device
  4. User C trusts device
  5. Verify: User A's entry should be removed (oldest), Users B and C remain

Test Case 4: Malformed Cookie Handling

  1. Manually set malformed cookie: hanko-device-token=invalid|data|here
  2. Login → Should gracefully handle (no crash), require MFA
  3. Trust device → Should set valid new cookie

Additional Context

Security Considerations

Aspect Status
Token Isolation ✅ Each user has their own token - cannot reuse another user's token
Cookie Security ✅ Existing HttpOnly, Secure, SameSite protections unchanged
User ID Exposure ✅ User IDs in cookie are UUIDs, already exposed in JWT
Cookie Size ✅ ~100 bytes per user entry, 4KB browser limit supports ~40 users
Malformed Data ✅ Invalid entries are skipped gracefully without affecting others

Cookie Size Analysis

Entry format: <UUID>:<base64_token>
UUID length: 36 characters
Token length: ~86 characters (64 bytes base64 encoded)
Separator: 1 character
Total per entry: ~124 characters

4KB cookie limit / 124 chars ≈ 32 users (conservative estimate)
Default limit of 20 provides comfortable margin

Browser Compatibility

The composite cookie format uses only standard ASCII characters (alphanumeric, -, =, |, :) and is compatible with all modern browsers' cookie handling.

When multiple users trust the same device/browser, the device trust cookie
was being overwritten, causing previous users to lose their trust status.

This change introduces a composite cookie format that stores multiple
user tokens in a single cookie: <user_id>:<token>|<user_id>:<token>

Features:
- New DeviceTrustMaxUsersPerDevice config option (default: 20)
- Backward compatible with legacy single-token format
- Automatic migration from legacy to new format
- Most recent user entry is prioritized (added to front)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant