Java Trusted Device Login: Secure 30-Day 2FA Skip via Rotating Tokens
This article details a complete Java implementation for trusted device login, covering secure token generation, server-side validation, hash storage, token rotation, revocation via MFA versioning, risk-based signals, and user-facing device management, ensuring 30-day 2FA bypass without compromising security.
After rolling out TOTP two-factor authentication, security improved but user experience suffered: high‑privilege users had to enter a 6‑digit code multiple times per day. The product team requested a "Trust this device for 30 days" checkbox to skip TOTP on known devices.
Implementing this correctly is not trivial. A naive design — storing a client‑generated deviceId and a trusted=true flag — would let an attacker copy the deviceId and permanently bypass 2FA. The article establishes a core rule: deviceId is only for identification and display, never a security credential. It can tell the server "this browser claims to be the previous device" but cannot prove it holds the server‑issued trusted credential.
Trusted Device Token
After a successful password + TOTP login, if the user checks "Trust this device 30 days", the server generates a cryptographically random secret (32 bytes, URL‑safe Base64 without padding). The full token sent to the client combines a trustedDeviceId prefix and the secret, e.g. TD_8e2f1c.k3Pl8sV.... The client stores the full token; the server stores only its hash (e.g. bcrypt or SHA‑256).
private static final SecureRandom RANDOM = new SecureRandom();
public String generateTrustedDeviceSecret() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}Database schema for trusted_device:
CREATE TABLE trusted_device (
id BIGINT PRIMARY KEY,
trusted_device_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
device_id VARCHAR(128),
device_name VARCHAR(255),
secret_hash VARCHAR(128) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
UNIQUE KEY uk_trusted_device_id (trusted_device_id)
);Even if the database leaks, the hashed secret cannot be used directly to bypass 2FA.
Why Not a Stateless JWT?
A 30‑day JWT with "trusted": true would avoid server‑side state, but revocation becomes impossible. If a user revokes a device in the security center, the JWT would remain valid until expiry. Therefore the token is only a proof‑of‑possession; the authoritative state ( ACTIVE, REVOKED, EXPIRED) lives on the server, enabling immediate revocation, admin cleanup, invalidation on password reset, and risk‑based invalidation.
Token Storage: HttpOnly Cookie
For web, the token is set in an HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/ cookie. This prevents ordinary JavaScript from reading the long‑term credential. Mobile apps should use Keychain/Keystore. In all cases the token is treated as a long‑term security credential, not a UI preference.
Login Flow with Trusted Device
User submits username/password (first factor).
Server checks for trusted_device cookie.
If present, split into trustedDeviceId and secret.
Look up active record by ID.
Verify: device.userId == currentUser, status == ACTIVE, expiresAt > now, hash(secret) == stored secretHash.
If all pass, skip TOTP and create login session.
If any check fails, fall back to TOTP challenge.
The flow changes from Password → TOTP → Session to
Password → Check Trusted Device → (if trusted) Session else TOTP → Session.
Trusted Device ≠ Permanent 2FA Removal
Trusted device only lowers friction for daily login. High‑risk operations (change password, disable 2FA, modify phone, delete passkeys, view recovery codes, change payment info) still require step‑up authentication (current TOTP or passkey). The system distinguishes login authentication from sensitive operation Step‑up Authentication .
Device Fingerprint as Risk Signal, Not Key
Collecting browser, OS, language, screen resolution, timezone, User‑Agent to compute a fingerprint is unreliable: updates, external monitors, language changes, privacy modes all alter the fingerprint, and parts can be spoofed. Fingerprint should only answer "does this login look anomalous compared to history?" not "is this the exact same device?" The trusted token remains the primary credential; device characteristics are merely risk context.
IP Changes Should Not Invalidate Trust
IPs change due to mobile networks, corporate NAT, VPNs, proxies. A simple if (!currentIp.equals(storedIp)) requireTotp() causes false positives. Instead, use risk scoring: same token + similar browser + usual region = low risk; token correct but sudden region jump + different User‑Agent + short‑time cross‑region = require TOTP. The token is the primary credential; environment changes are additional signals.
Token Rotation
To limit exposure if a token is stolen, rotate it on each successful use (like a refresh token). Current token T1 verified → invalidate T1 → issue T2 → update client cookie and server hash. If an old token ( T1) reappears later, treat as replay attack: revoke the trusted device and force TOTP. For low‑risk backends rotation may be optional, but high‑security systems should implement it.
Absolute Expiry vs. Sliding Window
If every login resets expiresAt = now + 30 days, a device used every few days stays trusted forever. Split into absoluteExpiresAt (hard 30‑day limit from first trust) and lastUsedAt (for idle timeout, e.g. revoke after 15 days of inactivity). This prevents indefinite trust extension.
Password Reset Revokes All Trusted Devices
When a user actively changes password (especially via forgot‑password or admin reset), all trusted devices are set to REVOKED. The rationale: password change signals a security event; previous long‑term trust must be re‑established. However, independent factors like Passkeys and TOTP secrets are not deleted — their lifecycles are separate.
Disabling 2FA Invalidates Trusted Devices via MFA Version
If a user disables and re‑enables TOTP, old trusted devices must not remain valid under the new 2FA configuration. Add an mfa_version column to sys_user (default 1) and to trusted_device. On every TOTP disable/re‑enable, security recovery, or admin MFA reset, increment mfa_version. During validation, if trustedDevice.mfaVersion != user.mfaVersion, treat as NOT_TRUSTED. This bulk‑invalidates all old devices without individual deletes.
ALTER TABLE sys_user ADD COLUMN mfa_version BIGINT NOT NULL DEFAULT 1;
ALTER TABLE trusted_device ADD COLUMN mfa_version BIGINT NOT NULL;
-- Validation
if (trustedDevice.mfaVersion() != user.mfaVersion()) {
return NOT_TRUSTED;
}
-- On MFA change
UPDATE sys_user SET mfa_version = mfa_version + 1 WHERE id = ?;User‑Visible Trusted Device List
If the system offers 30‑day TOTP skip, users must see which devices hold that privilege. Expose GET /api/security/trusted-devices returning device name, creation, last use, expiry, and a current flag. Allow revocation via DELETE /api/security/trusted-devices/{id} with server‑side ownership check ( trustedDevice.userId == currentUserId) and state transition ACTIVE → REVOKED.
Separate "Revoke Trust" from "Logout"
Revoke trust means future logins require TOTP; logout ends the current session. A user can revoke trust on their office PC while staying logged in; next login will need TOTP. A combined "Logout and revoke trust" action revokes both.
Admin Bulk Revocation
Admins can call POST /admin/users/{id}/revoke-trusted-devices. Instead of deleting rows, increment a trusted_device_generation or reuse mfa_version so all old devices fail validation on next use. Simultaneously revoke all sessions and refresh tokens to force full re‑authentication.
Clear Login Status Responses
The backend should return explicit status codes so the frontend knows the next step:
{ "status": "SECOND_FACTOR_REQUIRED", "challengeToken": "...", "availableMethods": ["TOTP", "RECOVERY_CODE"] }{ "status": "AUTHENTICATED" }on success.
Internal codes like TRUSTED_DEVICE_EXPIRED are mapped to user‑friendly messages ("Please verify your identity again for security") without exposing internal details.
Complete Login Flow
Username/Password
↓
First factor success
↓
Is MFA enabled?
├─ No → Create LoginSession
└─ Yes → Read Trusted Device Token
↓
Server validation
├─ Pass + low risk → Skip TOTP → Create LoginSession
└─ Fail/expired/revoked/version mismatch/high risk → Create SecondFactorChallenge → Require TOTP → Verify → Create LoginSession
↓
If user checked "Trust this device 30 days" → Create TrustedDevice → Issue Token → Set Cookie
↓
Create LoginSession → Access Token + Refresh TokenThe authentication system now layers cleanly:
First factor (password, SMS, QR, Passkey) → proves identity.
LoginSession → binds this login to a device.
Access Token + Refresh Token → API access and silent renewal.
TOTP → second factor for high‑risk accounts.
Trusted Device → allows known devices to skip daily TOTP without lowering overall account security.
What seemed like a simple checkbox is actually a server‑issued, revocable, expirable, rotatable long‑term credential that can bypass the second factor. It must be designed as a real authentication credential: random generation, server‑side state, secret hashing, explicit expiry, revocation support, rotation, risk detection, user visibility, and bulk invalidation on security events — not a localStorage.setItem("trusted", "true") .
When product says "this computer belongs to the user, skip TOTP for 30 days", the first question is not "where to put the checkbox?" but "how will the server prove this browser still holds the trusted credential issued after full MFA?" If the answer is only "same deviceId" or "same fingerprint", it is insufficient. True trust comes from a long‑term secret only that device possesses, paired with a server‑side revocable state. Fingerprints change, IPs change, browser versions change — but as long as the credential is valid, the server knows the client holds the token issued after MFA. And when the user revokes trust, the server can instantly invalidate that credential. That is what "Trust this device for 30 days" should actually deliver.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
