This episode strengthens sessions and tokens: short-lived sessions with id rotation, full logout on all devices, a trusted device option with a separate cookie, and JWT with an audience and expiry tied to the 2FA flag when issued.

2FA adds a layer at the gate, but the work isn't finished once the door opens. Episode 15 covers what happens after login: short-lived sessions, id rotation on authentication, a truly complete logout, a trusted device option, and JWT that records the 2FA status.
All of this determines how long a stolen session remains valuable. A long session without rotation turns one stolen code into permanent access — undoing the very value of the 2FA you just built. You'll close that problem on the session and token side.
Long sessions are convenient, but they also enlarge the window for abuse if a cookie leaks. Limit the session lifetime to 15-30 minutes for sensitive operations, with activity extending the session gradually. Never make 2FA permanent through a session that never ends.
Every time the authentication level rises — especially after MFA verification — change the session id. Rotation ensures an old cookie that may have been intercepted is no longer tied to the new session:
app.post('/login/mfa', async (req, res) => {
// ... verify the TOTP code ...
await new Promise((resolve) => req.session.regenerate(resolve));
req.session.userId = user.id;
res.json({ ok: true });
});req.session.regenerate issues a new session id and discards the old session data. Call it after the TOTP code validates — this is the most logical boundary to break the link with the old cookie.
Rotation done on a not-yet-complete session — for example at pendingMfa — must be distinguished: make sure rotation doesn't delete the pending status the login flow still needs.
Logging out only one device isn't enough when a session leaks. Provide a "log out all devices" option that removes the user's entire sessions from the session store. With express-session backed by a database store:
node -e "require('express-session'); const s = new (require('connect-sqlite3')(require('express-session')))(); s.destroyAllForUser('550e8400-e29b-41d4-a716-446655440000');"In production, use a structured query on the session table with a user_id column, deleting all rows belonging to the user. Wire this feature into the account security page, and automate it when the user rotates their secret or uses a recovery code.
Complete logout is also part of the flow when the 2FA secret changes — sessions created with the old 2FA shouldn't survive after 2FA is rotated.
The "remember this device for 30 days" option stores a dedicated cookie signed by the server, separate from the session. When the user returns with a valid trusted device cookie, the server can skip the TOTP challenge — but only after the password is correct:
const jwt = require('jsonwebtoken');
function issueTrustedDevice(userId) {
return jwt.sign(
{ sub: userId, purpose: 'trusted-device' },
process.env.SESSION_SECRET,
{ expiresIn: '30d' }
);
}The purpose: 'trusted-device' token distinguishes this cookie from other tokens, so a trusted device can't be abused for anything beyond its grant.
A trusted device may only skip the TOTP step, never replace the password. Don't apply trusted devices to sensitive operations like disabling 2FA or resetting the secret — those always demand the full code. And remember: a password alone isn't enough for a new device; a valid trusted cookie still requires the password.
Store the trusted device list as hashes of the tokens, not the raw tokens, so a table leak doesn't directly hand out valid cookies. Revoke device access one by one from the security page.
When an API uses JWT, the claims must be set strictly: aud limits the audience, exp limits the lifetime. A token without aud could be reused in a context where it shouldn't be:
const token = jwt.sign(
{ sub: user.id, mfa: user.totp_enabled, role: user.role },
process.env.JWT_SECRET,
{ audience: 'devvnull-api', expiresIn: '15m', issuer: 'devvnull-auth' }
);The mfa: user.totp_enabled claim tells the resource server whether the user went through MFA when the token was issued. This matters for layered authorization: a sensitive endpoint could demand a token with mfa: true.
A full token may only be issued after MFA completes. If a JWT flow is used, use the two-step approach like sessions: a temporary token with limited scope for the first step, a full token after the TOTP code validates. Never issue a full access token at /login when the user has 2FA.
If the resource server uses a token verification library, make sure it validates the audience and issuer on every request — not just the signature.
Episode 15 strengthened what happens after login: short sessions with id rotation, complete logout, trusted devices with a signed cookie that doesn't replace the password, and JWT with an audience, expiry, and a 2FA flag.
The key takeaways:
In the next episode, episode 16, we will cover testing: RFC test vectors and real-device E2E — validating TOTP with the official RFC 6238 vectors, using fake timers, and testing the full flow with a real Google Authenticator.