Skip to main content

How a User Logs In

This page is the long-form version of the introductory diagram. Follow it slowly. Every step is doing something specific.

The full flow

That is fifteen steps. Let's walk through them.

1. The user opens the login page

The browser fetches /login. Before showing anything, the page asks the API for a CSRF token. The CSRF page in the Security section explains why; for now, accept that the login form needs one.

2. CSRF token issued

The API returns a fresh CSRF token. The browser stores it both in a cookie and in JavaScript memory. The form will send the token both ways when it submits — the API compares them, and if they match, this is a real form submission and not a request forged by another website.

3. The user submits credentials

The form is sent over HTTPS only. In production, plain HTTP is rejected by HSTS. Even on localhost, login is protected from MITM by the local certificate.

4. Payload validation

Zod inspects the request body. Email must look like an email; password must be a non-empty string between sensible length limits. If validation fails, the API returns 400 immediately — no database query, no rate-limit increment, nothing. We do not waste resources on malformed input.

5. Rate limiting

The rate-limiter-flexible library, backed by Redis, checks two counters:

  • Per IP — has this IP made more than N login attempts in the last M minutes?
  • Per email — has this email received more than N attempts in the last M minutes?

The per-email counter exists because attackers rotate IPs. If both counters are under their limits, the attempt proceeds. If either is over, the API returns 429 Too Many Requests without checking the password at all.

6. Find the user

A MongoDB query lists the user with the given email. Two outcomes:

  • User exists, is active. Continue to step 7.
  • User does not exist, or is deactivated. The API still does a fake password verification (running Argon2 against a dummy hash) so the response time is the same as a real login attempt. This prevents an attacker from inferring "is this email registered?" by measuring how long the request takes. Then return 401.

7. Verify the password

The user document stores a hashed password — never the plaintext. The hash was generated by Argon2 when the password was set. To verify, Argon2 takes the submitted plaintext, applies the same algorithm with the same salt, and compares the result to the stored hash.

The Argon2 page in this section explains how the algorithm works and why it is the current best choice. The important property here is that the verification takes a deliberately measurable amount of time — around 100ms — to make brute-forcing the database hash extremely slow.

If the password matches, continue. If not, increment the rate-limit fail counter, write an audit log, and return 401.

8. Two-factor check

If the user has two-factor authentication enabled, the API does not create a session yet. It returns a 200 response with a flag saying "this user needs 2FA." The browser shows a code-entry form.

The user opens their authenticator app, types the six-digit code, and the browser POSTs it to /auth/2fa/verify. The API uses otplib to compute the expected code (which is a function of the shared secret stored in the user document and the current time, sliced into 30-second windows) and compares.

If the code matches, continue. If not, return 401 — and yes, this attempt is also rate-limited.

9. Create the session

The API creates a session record in Redis:

Key: session:<long-random-id>
Value: { userId, tenantId, createdAt, ip, userAgent }
TTL: 8 hours (configurable)

The session id is a long, cryptographically random string — 32 bytes, base64-encoded. There is no way to guess it.

10. Update the user document

The API updates the user's lastLoginAt timestamp and pushes an entry to their recent-logins array (capped to a small history). This is what powers the "your active sessions" list in the security tab.

11. Audit log

A row is written to the auditlogs collection: event=login.success, actor=<userId>, tenant=<tenantId>, ip, userAgent. The Audit Logging page in the Security section has the full story.

The API sends the session id back to the browser via a cookie. The cookie is set with three crucial flags:

FlagWhat it does
HttpOnlyJavaScript on the page cannot read this cookie. That means even if an XSS attack injects malicious JS, it cannot steal the session.
SecureThe cookie is sent only over HTTPS. (In production. In local dev, this flag is relaxed.)
SameSite=LaxThe browser will not send this cookie on cross-site requests, blocking many CSRF vectors.

The Sessions & httpOnly Cookies page goes deep on each of those flags.

13. The browser is now authenticated

From this point on, every request the browser makes carries the session cookie automatically. The API verifies the cookie on every request, looks up the session in Redis, attaches the user and tenant to the request, and continues.

If the session expires (Redis TTL elapses) or is revoked (user logs out, admin terminates it), the next request fails authentication and the user is redirected back to login.

What happens when the user logs out

The session is deleted from Redis immediately. Even if the cookie value somehow survives in the browser, the API will not find the session record and will reject the next request.

What happens on every subsequent request

This is the authentication middleware. It runs before every protected route. It is short, fast, and the gateway between "anonymous traffic" and "authenticated user." The next page goes deeper on what is in the cookie and why.

Key takeaways

  • A login is a fifteen-step dance involving CSRF, rate limiting, password verification, optional 2FA, session creation, an audit log, and a carefully-flagged cookie.
  • Failed logins do not give away whether the email exists (timing equalisation).
  • The session lives in Redis with an 8-hour TTL.
  • The session cookie is HttpOnly + Secure + SameSite=Lax — three independent protections.
  • Every later request flows through the authentication middleware that re-validates the session.