# authController.js

* * *

## 0\. Why this file exists at all

Think about what a web server actually receives when a request comes in: a URL, some headers (little key-value pairs like `Content-Type: application/json`), and maybe a body. That's it. There is no little tag attached to the request that says "this is Priya, and she's logged in." The server has to be *convinced*.

Every other file in this app — the one that lets lawyers post replies, the one that lets clients book appointments — eventually writes a line like:

```js
if (req.user.role === 'lawyer') { ... }
```

That line trusts `req.user` completely. It never asks "but how do we know this is really true?" **This file,** `authController.js`**, is the only place in the entire app responsible for answering that question.** Everywhere else just trusts the answer.

Why keep all of that logic in one file instead of spreading it around? Because if you ever need to change *how* identity is proven — swap JWTs for session cookies, add two-factor authentication, whatever — you want that to be a change to *one* file, not a hunt through fifty files that each independently decided to trust something. This idea has a name: **separation of concerns**. Each part of the app has exactly one job, and only one part of the app is allowed to decide "who is this person."

* * *

## 1\. The imports

```js
const bcrypt = require('bcryptjs');
const { createHash, randomUUID, timingSafeEqual } = require('crypto');
const jwt = require('jsonwebtoken');
```

Before touching what these *do*, slow down on the mechanics of `require`. In Node.js, `require('some-package')` runs that package's code and hands you back whatever it exported — usually one big object bundling together everything that package offers.

**Line 1.** `require('bcryptjs')` returns an object. That whole object gets stored in a variable named `bcrypt`. Later in the file you'll see `bcrypt.hash(...)` and `bcrypt.compare(...)` — those are just methods living on that object, accessed the normal way, with a dot.

**Line 2 is the one worth actually stopping on.** `require('crypto')` is Node's built-in cryptography toolbox, and it returns a genuinely huge object — dozens of functions for hashing, encryption, random number generation, and more. If you wrote:

```js
const crypto = require('crypto');
```

...you'd get the *whole* toolbox under the name `crypto`, and you'd write `crypto.createHash(...)` every time you needed it. Instead, the file does this:

```js
const { createHash, randomUUID, timingSafeEqual } = require('crypto');
```

The curly braces are **object destructuring**. Read it as: "Node, give me the whole crypto object, but then immediately reach inside it, pull out just these three named things, and give each one its own standalone variable name, matching what it's called inside the object." It is *exactly* equivalent to writing all of this:

```js
const crypto = require('crypto');
const createHash = crypto.createHash;
const randomUUID = crypto.randomUUID;
const timingSafeEqual = crypto.timingSafeEqual;
```

Four lines of manual unpacking, collapsed into one line of shorthand. The practical payoff: from this point in the file onward, you write `createHash(...)` directly — no `crypto.` prefix needed, because `createHash` is now its own independent variable holding that one function.

**Line 3.** Same simple case as line 1 — `require('jsonwebtoken')` returns one object, stored as `jwt`, giving you `jwt.sign(...)` (build a token) and `jwt.verify(...)` (check a token) later in the file.

**Why exactly these three from** `crypto`**, and nothing else?** Every single import in this file maps to one specific job later on, with zero waste:

| Import | Job in this file |
| --- | --- |
| `bcrypt` | hashing and checking *passwords* |
| `createHash` | hashing *refresh tokens* (a different job from passwords — explained in section 5) |
| `randomUUID` | generating a unique random ID for each refresh token |
| `timingSafeEqual` | safely comparing two hashes without leaking timing information |
| `jwt` | building and checking JSON Web Tokens |

Nothing here is unused. That's a small but real signal, when you're reading someone else's code, that the file was written deliberately rather than copy-pasted from a dozen tutorials.

*(This block only sets up tools — it has no inputs or outputs of its own to demonstrate. The functions below are what actually use these tools.)*

* * *

## 2\. The constants

```js
const ACCESS_TOKEN_TTL = '15m';
const REFRESH_TOKEN_TTL = '30d';
const MAX_REFRESH_SESSIONS = 10;
```

First, the plain English: these three lines just create three named values sitting at the top of the file, available to every function below them. `'15m'` means "15 minutes," `'30d'` means "30 days," and `10` is just the number ten.

**Why bother naming them, instead of just typing** `'15m'` **directly inside the function that needs it, three sections down?** Imagine you *didn't* name it — you typed `'15m'` directly inline. Six months later, your team decides access tokens should last 30 minutes instead of 15. You'd now have to go find every single place in the codebase that happens to type the literal string `'15m'`, and hope you found all of them, and hope none of them meant something unrelated that just happened to also be `'15m'`. This gets error-prone fast. By instead writing `ACCESS_TOKEN_TTL` once at the top and referencing that name everywhere else, changing the policy becomes a single-line edit, and every place that uses it picks up the new value automatically, guaranteed. This principle — say a value once, refer to it by name everywhere else — is called **DRY: Don't Repeat Yourself**. Here it's being applied not to *logic* but to a *policy decision* (how long should things last), which is just as important to keep in one place.

**What actually *is*** `'15m'`**, mechanically?** It isn't a number of seconds you calculate yourself. It's a small duration string that gets handed to the `jsonwebtoken` library later (in `signAccessToken`, section 3), and that library has its own tiny internal parser that understands strings like `'15m'`, `'2h'`, `'30d'`, `'7 days'`, and converts them into a plain count of seconds behind the scenes. You will never see that conversion happen directly in this file — it's hidden inside `jwt.sign` — but it's worth knowing it's happening, because the *next* thing it does with that number is the actual mechanism behind "token expiry," which is just:

> take the current time, add the duration, store that future moment as a plain number (seconds since January 1st, 1970 — this is called a "Unix timestamp"), and stamp it into the token as a field named `exp`.

Later, checking whether a token has expired is nothing magical — it's just: "is `exp` (a number) less than `Date.now()` (another number)?" You'll meet that check being *performed* automatically inside `jwt.verify()` in section 10.

**Why is** `MAX_REFRESH_SESSIONS` **set to exactly** `10`**, and why does that connect to MongoDB specifically?** Here's a fact about MongoDB that has nothing to do with this app specifically: every single document stored in MongoDB — no matter what's in it — has a hard ceiling of **16 megabytes**. You will basically never hit that limit with normal fields like a name or an email address. But this app's design stores a list called `refreshTokens` *directly inside* each user's document — one entry every time that person logs in, from any device. If nothing ever removed old entries, a user who logged in daily for ten years, across multiple browsers and phones, would have a document that just kept growing, forever, with no upper bound. `10` is this file's answer to the question "how many devices does one real person plausibly use at once?" — a login limit that also, as a side effect, guarantees this array can never come close to threatening that 16MB ceiling. The section-by-section mechanism that actually *enforces* this number lives in `addRefreshSession` (section 7) — this constant is just the policy; that function is the enforcement.

*(Again, no direct input/output here — these are just labelled values other functions will use.)*

* * *

## 3\. `signAccessToken` — turning a user into a short-lived proof of identity

```js
function signAccessToken(user) {
  return jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: ACCESS_TOKEN_TTL }
  );
}
```

**What is this function's job, in one sentence?** Given a user object (something already fetched from the database), produce a short string — the "access token" — that this user's browser can hand back on every future request as proof of "I already logged in, here's my receipt."

**Reading** `jwt.sign`**'s three arguments slowly:**

1.  **The payload** — `{ userId: user._id, role: user.role }`. This is the actual data you want carried inside the token. Just two fields: which user this is, and what role they have (`'client'` or `'lawyer'`).
    
2.  **The secret** — `process.env.JWT_ACCESS_SECRET`. This is a string pulled from an **environment variable**, not typed directly into the code. Why does that distinction matter? If the secret were typed directly as a string in this file, it would get committed to source control (git) the moment someone ran `git push` — visible to anyone with repo access, forever, in the history, even if you later remove it. Environment variables live only on the actual running server, configured separately for development, staging, and production, and are never written into any file that gets committed. This secret is the single most important piece of information in this entire authentication system — anyone who obtains it can forge tokens for any user, including an admin.
    
3.  **Options** — `{ expiresIn: ACCESS_TOKEN_TTL }`. This is where `'15m'` from section 2 finally gets used, and where it becomes that `exp` timestamp described a moment ago.
    

**What comes back out** is a JWT: a string made of three parts separated by dots, each part encoded in a text-safe format called base64url — a header, the payload, and a signature.

**The single most important security idea in this whole function, and the one that trips up nearly everyone reading JWT code for the first time:** *the payload is signed, not encrypted.* Signing and encrypting sound similar but do opposite things. Encrypting hides content so only someone with the right key can read it. Signing does *not* hide anything — it just attaches a tamper-proof seal. Anyone at all — a browser extension sitting on the user's machine, a proxy server somewhere on the network, literally anyone who gets a copy of this string — can decode the middle chunk with nothing more than a plain base64 decoder (no secret required) and read the payload in plain text: `{"userId": "64f...", "role": "client", "exp": 1735689600}`. The secret's *only* job is making the third chunk — the signature — impossible to forge. If an attacker tried editing the decoded payload to say `"role": "admin"` and then re-encoded it, the signature would no longer match what `jwt.verify` recomputes on the way back in (section 10), and the tampered token would be rejected outright. **The practical rule this teaches you: never put a password, a credit card number, or anything genuinely secret inside a JWT payload.** Think of it as a sealed glass envelope — sealed against tampering, but transparent to anyone who wants to look.

**Why include** `role` **in this particular token, given that principle?** This is a deliberate trade-off, not an oversight. Because `role` sits right there in the token, some other file's middleware — say, a check like `requireRole('lawyer')` guarding a lawyer-only page — can make its decision just by reading the token, without needing to ask the database "what is this person's role?" on every single request. That's a real performance win. The cost: if an admin changes someone's role while they're still logged in, their *existing* token keeps claiming the *old* role until it naturally expires — which, thanks to `ACCESS_TOKEN_TTL`, is a maximum of 15 minutes later. That bounded delay is the price paid for not hitting the database on every request.

### Worked example

**Input** (a user document already loaded from MongoDB):

```js
const user = {
  _id: "64f1a2b3c4d5e6f7a8b9c0d1",
  role: "lawyer",
  name: "Amara Chen"
};

signAccessToken(user);
```

**Output** (a single string — line breaks added here only for readability; the real thing is one continuous line):

```plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiI2NGYxYTJiM2M0ZDVlNmY3YThiOWMwZDEiLCJyb2xlIjoibGF3eWVyIiwiaWF0IjoxNzM2MjAwMDAwLCJleHAiOjE3MzYyMDA5MDB9.
Xk3pQz7...(signature bytes)...
```

If you took the middle chunk and ran it through any online base64 decoder, you would see, in plain readable text — no secret needed:

```json
{ "userId": "64f1a2b3c4d5e6f7a8b9c0d1", "role": "lawyer", "iat": 1736200000, "exp": 1736200900 }
```

`exp` here is `1736200900` — 900 seconds (15 minutes) after `iat` (the "issued at" timestamp), which is exactly what `ACCESS_TOKEN_TTL = '15m'` produced.

* * *

## 4\. `signRefreshToken` — a longer-lived, lower-power proof

```js
function signRefreshToken(user) {
  return jwt.sign(
    { userId: user._id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: REFRESH_TOKEN_TTL, jwtid: randomUUID() }
  );
}
```

This function looks almost identical to the last one — same three arguments to `jwt.sign` — which is exactly why it's worth reading it *against* `signAccessToken`, difference by difference, because every single difference here was a deliberate decision.

**Difference 1 — the payload only has** `userId`**, no** `role`**.** Think about what a refresh token is *for*: its one and only job, later, is to be traded in for a brand-new access token (you'll see this in section 10). It should never, by itself, be usable to make an authorization decision like "is this person allowed to see this lawyer-only page?" Giving it the absolute minimum information it needs to do its one job, and nothing more, is called the **principle of least privilege** — a token that leaks should leak as little useful information as possible.

**Difference 2 — a completely different secret,** `JWT_REFRESH_SECRET`**, not** `JWT_ACCESS_SECRET`**.** Why maintain two separate secrets instead of reusing one everywhere? Picture the worst case: somehow, one of these two secrets leaks (a misconfigured server, a leaked `.env` file, whatever). If both token types used the same secret, an attacker who obtained it could now forge *both* access tokens and refresh tokens for any user. With two separate secrets, a leak of one only compromises that one token type — the blast radius is cut in half. This is called **token type separation**.

**Difference 3 — the expiry is** `'30d'`**, not** `'15m'`**.** This is the whole reason refresh tokens exist at all: nobody wants to type their password again every 15 minutes. The access token is short-lived on purpose (so a stolen one goes stale fast); the refresh token is long-lived on purpose (so a real user stays logged in for weeks without re-entering credentials), and it's used sparingly — only to mint new access tokens, not on every single request.

**Difference 4 — a brand new option that didn't exist before:** `jwtid: randomUUID()`**.** This one deserves the slowest read of the whole function.

`jwtid` sets a field inside the token called `jti` (JWT ID) — a random, unique identifier baked into *this one specific act of signing a token*. `randomUUID()` comes from Node's built-in `crypto` module and produces a string that looks like `f47ac10b-58cc-4372-a567-0e02b2c3d479` — a **cryptographically random** value, meaning it isn't just "unlikely to repeat," it's astronomically unlikely to ever repeat, by design.

**Why is this necessary at all?** Picture a genuinely realistic scenario: one user logs into the app from their laptop and their phone within the same second — not an attack, just an ordinary person doing an ordinary thing. Both logins call `signRefreshToken(user)` with the *same* `user` object, meaning the exact same payload (`{ userId: ... }`), the exact same secret, and possibly the exact same second for `iat`/`exp`. Without anything else varying between them, it becomes at least theoretically possible for two signed token strings to end up identical or predictably related to one another. That would quietly break an assumption the rest of this file leans on hard, starting two functions from now: *that each stored hashed token uniquely identifies exactly one session.* Adding `randomUUID()` into the mix guarantees every single call to `signRefreshToken`, no matter the user, no matter the timing, produces a token that has never existed before, anywhere, and never will again.

### Worked example

**Input:**

```js
const user = { _id: "64f1a2b3c4d5e6f7a8b9c0d1" };

signRefreshToken(user);
```

**Output** (a JWT string, structurally like before but different payload/secret):

```plaintext
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VySWQiOiI2NGYxYTJiM2M0ZDVlNmY3YThiOWMwZDEiLCJqdGkiOiJmNDdhYzEwYi01OGNjLTQzNzItYTU2Ny0wZTAyYjJjM2Q0NzkiLCJpYXQiOjE3MzYyMDAwMDAsImV4cCI6MTczODc5MjAwMH0.
9zLm2q...(different signature bytes)...
```

Decoding the middle chunk shows:

```json
{
  "userId": "64f1a2b3c4d5e6f7a8b9c0d1",
  "jti": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "iat": 1736200000,
  "exp": 1738792000
}
```

Notice: no `role` field at all (Difference 1), and `exp` is roughly 30 days after `iat`, not 15 minutes (Difference 3). Call this function again for the *same* user a second later, and `jti` will be a completely different random string, even though `userId` is identical — that's `randomUUID()` doing its job.

* * *

## 5\. `digestRefreshToken` — turning a long token into a fixed, lookup-friendly fingerprint

```js
function digestRefreshToken(refreshToken) {
  return createHash('sha256').update(refreshToken, 'utf8').digest('hex');
}
```

Before anything else: why hash the refresh token *at all* instead of just storing the raw token string in the database? Think about what the database contains if you store the raw token: if that database were ever stolen or leaked (a genuine, common category of real-world breach), the attacker would have every user's live refresh token in hand, ready to use immediately, with zero extra work. Storing a *hash* of the token instead means the database leak alone isn't enough — you'd need to reverse a one-way hash, which is (by design) computationally infeasible.

**Reading the three chained method calls one at a time:**

1.  `createHash('sha256')` — asks Node's crypto module for a fresh, empty "hash object," specifically configured to use the SHA-256 algorithm. Nothing has been hashed yet at this point; this just prepares the tool.
    
2.  `.update(refreshToken, 'utf8')` — feeds the actual token string into that hash object, telling it to interpret the string as UTF-8 encoded bytes (the standard way JavaScript strings are turned into raw bytes).
    
3.  `.digest('hex')` — tells the hash object "you're done receiving input, now give me the final result," and asks for that result formatted as a hexadecimal string (characters `0-9` and `a-f`). SHA-256 always produces exactly 256 bits of output, and 256 bits written in hex is always exactly **64 characters** — that specific number, 64, becomes important again in the very next function.
    

**The core property to understand: this hash is deterministic.** Feed the exact same string in a thousand times, get the exact same 64-character output a thousand times, no exceptions, no randomness involved anywhere.

**Now contrast that directly with** `bcrypt.hash`**,** which you'll meet properly in `register` (section 8), because the contrast is the entire point of this section. `bcrypt.hash` is deliberately **non-deterministic** — it generates a random "salt" internally every single time it's called, so hashing the identical password twice produces two different-looking results both times.

**Why does a password need randomness while a refresh token specifically must *not* have it?** It comes down to what each hash needs to be used *for*, later:

*   A password hash needs to resist an attacker who has stolen the *entire* database and now has unlimited offline time to try to crack it, possibly using **rainbow tables** — huge, precomputed lookup tables mapping common passwords to their hashes. If every user's password were hashed the same deterministic way, two users who happened to share the password `"password123"` would have the *identical* stored hash — and cracking one instantly reveals the other. Randomizing per-user defeats that.
    
*   A refresh token, by contrast, needs *fast, exact* lookup: "does any currently active session for this user match the token that was just presented?" To answer that efficiently, the stored value needs to be reproducible — hash the incoming token the same way every time, then simply compare it against what's stored. If refresh tokens used bcrypt instead, checking one incoming token against, say, ten stored sessions would require calling the (deliberately slow) `bcrypt.compare()` up to ten separate times, on every single refresh request from every user. The file's own design notes cite this specific performance problem as the actual reason an earlier version of this code, which did use bcrypt here, was abandoned.
    

**There's also a sharper, genuinely-a-correctness-bug reason bcrypt would be actively wrong here, not merely slower:** bcrypt silently truncates any input longer than 72 bytes — anything past that length is simply ignored during hashing. A JWT can easily exceed 72 bytes. That means two *genuinely different* long refresh tokens that happen to share the same first 72 bytes could be hashed by bcrypt into *matching* results — a false positive that would let the wrong token pass as valid. SHA-256 has no truncation limit like this at all; it processes an input of any length in full, so this entire failure mode simply cannot happen with it.

### Worked example

**Input:**

```js
digestRefreshToken("eyJhbGciOiJIUzI1NiIs...signRefreshToken output from above...");
```

**Output** (always exactly 64 hex characters, always the same for this exact input string):

```plaintext
7a3f9b2e1c6d4a8f0b5e2d9c1a7f4e6b8d3c0a9f2e5b7d1c4a6f8e0b3d5c7a91
```

Run it again on the exact same input string, get the exact same 64 characters back. Change even one character of the input — say, one letter in the token — and the output changes completely and unpredictably (this is called the **avalanche effect**), for example something like:

```plaintext
f1e4c7a0b3d6f9e2c5a8b1d4f7e0c3a6b9d2f5e8c1a4b7d0f3e6c9a2b5d8f1e4
```

* * *

## 6\. `refreshTokenMatchesDigest` — safely checking "does this token match what's stored?"

```js
function refreshTokenMatchesDigest(refreshToken, storedDigest) {
  if (typeof storedDigest !== 'string' || !/^[a-f0-9]{64}$/i.test(storedDigest)) {
    return false;
  }
  const presented = Buffer.from(digestRefreshToken(refreshToken), 'hex');
  const stored = Buffer.from(storedDigest, 'hex');
  return timingSafeEqual(presented, stored);
}
```

**The guard clause on the first line, read slowly.** `/^[a-f0-9]{64}$/i` is a **regular expression** — a pattern used to test whether a string has a certain shape. Breaking the pattern down symbol by symbol:

*   `^` means "the match must start at the very beginning of the string" — no extra characters allowed before it.
    
*   `[a-f0-9]` means "any single character that is a lowercase letter from `a` to `f`, or a digit from `0` to `9`" — exactly the character set a hex digest is made of.
    
*   `{64}` means "exactly 64 of the previous thing in a row" — not 63, not 65.
    
*   `$` means "and that must be the very end of the string" — no extra characters allowed after it either.
    
*   The trailing `i` flag makes letter matching case-insensitive, so uppercase `A-F` would also pass.
    

Put together: "this string must be exactly 64 characters long, and every single one of them must be a hex digit — nothing more, nothing less." `.test(storedDigest)` runs `storedDigest` against that pattern and returns plain `true` or `false`.

**Why does this check exist at all?** Two overlapping reasons. First, it's a cheap sanity filter — from section 5, you already know a real SHA-256 hex digest is *always* exactly 64 characters, so anything that doesn't match this shape genuinely cannot be a valid digest, and there's no point doing any further, more expensive work on it. Second, and more concretely, it solves a real migration problem: imagine this app *used* to hash refresh tokens with bcrypt (as the earlier, abandoned version mentioned in section 5 did) — bcrypt hashes look like `$2b$12$N9qo8uLOickgx2ZMRZoMy...`, nowhere near 64 plain hex characters. Old session records left over from that earlier version, still sitting in the database, would fail this check immediately and safely return `false` — treated as simply invalid — instead of crashing the function, or worse, silently being fed into a comparison that assumes a shape they don't actually have.

**Lines 3–4 convert text into raw bytes.** `Buffer.from(hexString, 'hex')` takes a string like `"7a3f9b2e..."` and turns it into an actual sequence of raw binary bytes (a `Buffer`, Node's representation of raw binary data) — because the comparison on the next line needs to operate on real bytes, not on text characters.

**Line 5 is the security-critical line in this whole function.** `timingSafeEqual(a, b)` looks, at a glance, like it should do exactly what `a === b` does — check whether two things are equal. It does *not* work the same way underneath, and the difference matters enormously here.

Picture what `a === b` (or a hand-written loop comparing byte by byte) does internally: it compares the first byte; if that byte doesn't match, it **stops immediately** and returns `false`, without bothering to check any of the remaining bytes. That "stop as soon as you find a mismatch" behavior is normally a totally reasonable, harmless optimization. But it creates a subtle, measurable side effect: a guess that gets its *first* byte wrong returns `false` microscopically faster than a guess that gets the first ten bytes right and only fails on the eleventh — because more work had to happen before the second one could fail. To one single human eyeballing one single request, that time difference is completely invisible — we're talking fractions of a millisecond. But an attacker who can send *millions* of automated, precisely-timed requests, and carefully measure the response time of each one, can statistically detect that pattern and use it to reconstruct a correct secret **one byte at a time**, entirely without ever guessing the whole thing at once. This category of attack has a name: a **timing attack**.

`timingSafeEqual` is built specifically to defend against this: it always compares *every single byte*, every time, with no early exit, so the total time the comparison takes reveals absolutely nothing about how close a wrong guess was.

**The general rule this function is teaching:** any time your code compares a secret against a stored value — a token, a cryptographic signature, an API key — always use a timing-safe comparison, never `===` and never a hand-rolled loop. The one common, deliberate exception in this exact file is comparing *passwords*, and that's only safe because `bcrypt.compare()` (section 9) is already timing-safe internally, by construction — you don't need to wrap it in anything extra.

### Worked example

**Input (a genuine match):**

```js
refreshTokenMatchesDigest(
  "eyJhbGciOi...(the real refresh token)...",
  "7a3f9b2e1c6d4a8f0b5e2d9c1a7f4e6b8d3c0a9f2e5b7d1c4a6f8e0b3d5c7a91"
);
```

**Output:** `true` — because hashing that exact token with `digestRefreshToken` reproduces that exact 64-character string.

**Input (a stale, no-longer-matching digest — imagine the session was rotated already, so the stored digest changed to something new):**

```js
refreshTokenMatchesDigest(
  "eyJhbGciOi...(the same old refresh token)...",
  "f1e4c7a0b3d6f9e2c5a8b1d4f7e0c3a6b9d2f5e8c1a4b7d0f3e6c9a2b5d8f1e4"
);
```

**Output:** `false` — the hash of the presented token doesn't equal the stored digest.

**Input (a leftover bcrypt-format record from before the migration):**

```js
refreshTokenMatchesDigest(
  "eyJhbGciOi...(some token)...",
  "$2b$12$N9qo8uLOickgx2ZMRZoMy.eIkP1z4Y1a2b3c4d5e6f7g8h9i0j1"
);
```

**Output:** `false`, and immediately — this fails the regex shape check on line one and never even reaches `timingSafeEqual`.

* * *

## 7\. `addRefreshSession` — recording a new logged-in device, safely

```js
async function addRefreshSession(user, refreshToken, device) {
  const tokenHash = digestRefreshToken(refreshToken);
  const result = await User.updateOne(
    { _id: user._id, isActive: true },
    {
      $set: { lastLogin: new Date() },
      $push: {
        refreshTokens: {
          $each: [{ tokenHash, device: device || 'unknown', createdAt: new Date() }],
          $slice: -MAX_REFRESH_SESSIONS,
        },
      },
    }
  );
  if (result.modifiedCount !== 1) throw new Error('Could not persist refresh session.');
}
```

**Big picture first:** this function's job is to take a brand-new refresh token that was just issued, and record its *hash* (never the raw token itself — you already know why from section 5) into that user's document in the database, as one entry in their `refreshTokens` list, while also updating their "last logged in" timestamp.

**The filter,** `{ _id: user._id, isActive: true }` **— this is the single most important line in the whole file to understand deeply, because the pattern it demonstrates should become a reflex you reuse constantly.**

Here's the naive, tempting-but-wrong alternative you might write instead: somewhere earlier in the code, you already fetched this user from the database and confirmed, in a plain JavaScript `if` statement, that `user.isActive === true`. Then, later, you run this update using *only* `{ _id: user._id }` as the filter — no `isActive` check *inside* the update itself, because "I already checked that a moment ago."

The problem is the phrase "a moment ago." Between the instant you *checked* `isActive` in memory and the instant this update actually runs against the database, real time passes — however small. In that gap, it is entirely possible for a *different*, concurrent request to run — for example, an admin, in a completely separate request happening at the same time, deactivating this exact account. If that happens in the gap, your update would still go through, because your code never re-checks anything — it just trusts the answer it got a moment earlier. The result: you'd have silently created a brand-new, fully valid login session for an account that is, at this very instant, supposed to be deactivated. This category of bug has a name: a **check-then-act race condition**, or sometimes **TOCTOU** (time-of-check to time-of-use).

The fix, which is exactly what this line does, is to fold `isActive: true` directly **into the same filter as the write itself**. MongoDB re-evaluates that entire filter — including `isActive: true` — at the exact instant it performs the update, not a moment earlier, not based on some stale value you checked previously. If the account was deactivated even one millisecond before this exact line executes, the filter simply matches zero documents, and no write happens at all. This is called an **atomic filter-and-update** — "check and act" collapsed into one indivisible database operation instead of two separate steps with a gap between them. The general lesson to carry forward: whenever a condition genuinely matters for whether a write should happen, put that condition *inside* the same atomic operation as the write — never check it separately, beforehand, in application code, and trust that nothing changed in between.

`$set: { lastLogin: new Date() }`**.** `new Date()` captures whatever the current moment is, according to the server's clock, at the exact instant this line executes. It's stored on the document as a real BSON date value (MongoDB's native date type), not as a plain text string — which matters, because a real date type can later be sorted, filtered, and compared using normal date operators (`$gt`, `$lt`, etc.), whereas a string like `"2026-09-07"` would need to be parsed back into a real date every time before any of that would work correctly. This line runs on *every* login, which means the app always has an accurate "last seen" timestamp for every user, at essentially zero extra cost.

`$push` **combined with** `$each` **and** `$slice` **— this is the mechanism that actually enforces the** `MAX_REFRESH_SESSIONS` **policy from section 2.** Read it from the inside out:

*   On its own, `$push` appends exactly one new item to an array field.
    
*   `$each` changes that to "append a *list* of items in one call" — here, the list happens to contain exactly one new session object, but the syntax requires `$each` regardless, because `$slice` (below) can only be used alongside it, even when pushing a single item.
    
*   `$slice: -MAX_REFRESH_SESSIONS` — which is `-10` — then trims the *entire resulting array* down to keep only the **last 10 entries**. Negative numbers in `$slice` specifically mean "count backward from the end of the array," so `-10` means "keep the final 10 items, discard everything before them."
    

Put together: every new login appends a new session entry at the end, and the moment the array would exceed 10 entries, the oldest one (or ones) are silently and automatically dropped from the front. This is a classic computer-science pattern called a **bounded queue** — new items always enter one end, and once the structure is "full," adding a new item pushes the oldest one out. Concretely, this means an 11th device logging in doesn't make the array grow to 11 — it makes the array *stay* at 10, quietly evicting whatever session had been sitting there longest.

`device: device || 'unknown'`**.** The `||` operator here isn't being used for boolean logic ("is this true or that true") — it's being used for its **short-circuit fallback behavior**: if the left-hand side (`device`) is "falsy" — meaning `undefined`, `null`, `''` (empty string), or a few other values JavaScript treats as false-like — the whole expression evaluates to whatever's on the right-hand side instead, `'unknown'`. Where does `device` come from? Looking ahead to `register` (section 8), it's read from `req.headers['user-agent']` — and an HTTP client is technically allowed to send a request with *no* User-Agent header at all. Without this fallback, a client that omitted that header would cause `device` to end up `undefined`, and the database would end up storing the literal value `undefined` in that field — which is exactly the kind of inconsistent data this one small fallback exists to prevent.

**The final line:** `if (result.modifiedCount !== 1) throw ...`**.** This is about correctly reading what `updateOne` actually tells you. It's tempting to assume "if the call didn't throw an exception, it must have worked" — but that's not what MongoDB's `updateOne` promises. Its return value is an object reporting, among other things, how many documents *matched* the filter (`matchedCount`) and how many were *actually modified* (`modifiedCount`) — and critically, if the filter matches zero documents (exactly the scenario from a few paragraphs ago, where `isActive` had already flipped to `false`), `updateOne` does **not** throw anything at all. It succeeds, at the plain JavaScript level, having matched and changed precisely nothing. This line is what turns "the database call technically didn't crash" into "the database call actually did what I needed it to do" — checking `modifiedCount` specifically, rather than just the absence of a thrown error, is the difference between *assuming* an outcome and *verifying* one.

### Worked example

**Input:**

```js
const user = { _id: "64f1a2b3c4d5e6f7a8b9c0d1", isActive: true };
const refreshToken = "eyJhbGciOi...(a freshly signed refresh token)...";
const device = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)";

await addRefreshSession(user, refreshToken, device);
```

**What happens internally, step by step:**

1.  `digestRefreshToken(refreshToken)` produces something like `"7a3f9b2e1c6d4a8f0b5e2d9c1a7f4e6b8d3c0a9f2e5b7d1c4a6f8e0b3d5c7a91"`.
    
2.  MongoDB looks for a document where `_id` equals `"64f1a2b3c4d5e6f7a8b9c0d1"` **and** `isActive` is currently `true`.
    
3.  Assuming it finds one, it sets `lastLogin` to the current timestamp, and pushes this new object onto `refreshTokens`:
    
    ```js
    { tokenHash: "7a3f9b2e...c7a91", device: "Mozilla/5.0 (iPhone...)", createdAt: <now> }
    ```
    
4.  If the array now has, say, 11 entries because this was an 11th device, `$slice: -10` trims it back down to the 10 most recent, dropping the oldest one.
    

**Output:** the function returns `undefined` (it has no `return` value) on success — its real effect is the side effect on the database. If the account had been deactivated a moment before this ran, `result.modifiedCount` would be `0`, and instead of quietly returning, the function would throw:

```plaintext
Error: Could not persist refresh session.
```

* * *

## 8\. `register` — walked one line at a time, in the exact order it executes

```js
const { name, email, phone, password, role } = req.body;
```

Same destructuring mechanic you saw explained slowly in section 1, now applied to `req.body` (whatever JSON the client sent) instead of to a package. Without destructuring, you'd write five separate lines: `const name = req.body.name;`, `const email = req.body.email;`, and so on. This one line does all five at once — and as a nice side effect, it also functions as instant, always-accurate documentation of exactly which fields this endpoint expects, right at the top of the function.

```js
if (!name || !email || !phone || !password || !role) {
  return res.status(400).json({ message: "Missing required fields." });
}
```

This is **fail-fast validation**, and specifically, the *ordering* is deliberate: this check uses nothing but values already sitting in memory — no network call, no database query — so it's essentially free to run, and it runs *before* any of the more expensive steps below it (which do talk to the database). `!name` evaluates to `true` exactly when `name` is falsy (missing, empty string, etc.); chaining five of these with `||` means the whole condition becomes `true` the moment **any single one** of the five fields is missing. The HTTP status code `400` specifically means "the request itself is malformed" — there's no point asking the database anything yet, because the request wouldn't make sense even in a perfect world with a perfect database.

```js
if (!["client", "lawyer"].includes(role)) {
  return res.status(400).json({ message: "Invalid role." });
}
```

`["client", "lawyer"].includes(role)` checks whether the string in `role` is one of exactly those two values, and nothing else — this is called a **whitelist** (an explicit list of what *is* allowed) as opposed to a **blacklist** (a list of what *isn't*). Why write this check at all, if the actual signup page's dropdown menu only ever shows those two choices to a real user clicking through the UI? Because **the frontend is not a security boundary.** Nothing about how the website's dropdown is built stops someone from bypassing the website entirely and sending a raw HTTP request directly to this endpoint — using a tool like curl or Postman — with the body `{"role": "admin", ...}`. The dropdown only constrains people using the actual web page; this one line of server-side code is what actually constrains *everyone*, regardless of how they're sending the request.

```js
const existing = await User.findOne({ $or: [{ email }, { phone }] });
if (existing) {
  return res.status(409).json({ message: "Email or phone already registered." });
}
```

`$or` is a MongoDB query operator meaning exactly what it sounds like: "find one document where the email field matches this value, **or** the phone field matches this value." `409 Conflict` is the HTTP status specifically meaning "this request conflicts with the current state of whatever it's trying to create."

Now, the honest concurrency reality of this check, which is important to understand rather than assume: **this is a check-then-act race condition, structurally identical in shape to the one explained in section 7 — sometimes called TOCTOU (time-of-check to time-of-use).** Picture two registration requests for the exact same email address arriving at nearly the same instant. Both `findOne` queries can run and come back completely empty — "no existing user found" — *before either request has actually inserted anything into the database yet*. Both requests, having each seen an empty result, now believe the email is free, and both proceed forward. **This line, by itself, does not prevent two accounts from ending up with the same email.** The actual backstop for that has to live one layer deeper, at the database schema level: a **unique index** on the `email` and `phone` fields. With that index in place, when the second (racing) request eventually reaches `User.create()` a moment later, MongoDB itself refuses the insert outright and throws a specific, recognizable **duplicate-key error (error code 11000)** — and *that* is the real guarantee against duplicates, not this `findOne` check. This `findOne` exists purely to produce a friendlier, more specific error message in the common, everyday, non-racing case — it is explicitly, by design, not the actual safety net.

```js
const passwordHash = await bcrypt.hash(password, 12);
```

This is the promised contrast to `digestRefreshToken` from section 5 — the non-deterministic counterpart. `bcrypt.hash(plaintext, saltRounds)` takes two arguments: the plain password, and a "cost factor." `12` here means the underlying algorithm runs `2^12` (4,096) internal iterations. Deliberately raising this number makes the hashing operation slower — and that slowness is the entire point. It costs your server a negligible, barely-perceptible fraction of a second to hash *one* password during *one* real user's registration. But it costs an attacker who has stolen your entire database of hashes and is trying to guess passwords offline, billions of times over, an enormous, deliberately inflated amount of total compute time. `12` is a widely used, well-tested balance point between those two costs. Crucially, and unlike `digestRefreshToken`, **bcrypt also automatically generates and embeds a random "salt" into its output on every single call** — hash the identical password twice, and you'll get two different-looking hash strings both times. That single property is precisely why checking a password later, in `login` (section 9), can't just compare two hash strings with `===` — it has to use `bcrypt.compare()` instead, which specifically knows how to extract that embedded salt back out of the stored hash before recomputing anything.

```js
const user = await User.create({ name, email, phone, passwordHash, role });
```

This uses **shorthand property syntax** — the writing-side mirror of the destructuring you saw on the reading side back at the top of this function. Because each variable name here (`name`, `email`, `phone`, `passwordHash`, `role`) already exactly matches the key name you want in the resulting object, JavaScript lets you skip repeating yourself: `{ name, email, phone, passwordHash, role }` means precisely the same thing as writing out `{ name: name, email: email, phone: phone, passwordHash: passwordHash, role: role }` in full. `User.create()` is a single Mongoose method that quietly does three separate things for you in one call: builds a complete document in memory, runs every validation rule defined on the schema (correct types, required fields present, any custom rules), and then actually saves it to MongoDB — from the point of view of the code calling it, this feels like one atomic step, even though multiple things are happening underneath.

```js
if (role === "client") {
  await Client.create({ userId: user._id });
} else {
  await Lawyer.create({ userId: user._id, specialization: [], yearsOfExperience: 0, consultationFee: 0 });
}
```

This is a data-modeling decision worth understanding on its own terms, sometimes called **profile fan-out**. The `User` document itself only holds what's genuinely common to *every* account, regardless of role: identity and login credentials — name, email, phone, password hash, role. Anything that's specific to *one particular role* — a lawyer's list of legal specializations, their years of experience, their consultation fee — lives instead in a completely separate document, in a separate MongoDB collection (`Lawyer` or `Client`), linked back to the user only by storing that user's `_id` as a field named `userId`. This is exactly why, elsewhere in this app, "a lawyer" conceptually corresponds to *two different IDs* referring to the same real person: `User._id` (their login identity) and `Lawyer._id` (their profile document) — meaning some other piece of code that needs the lawyer's profile has to perform a *second* lookup, something like `Lawyer.findOne({ userId: someUsersId })`, to bridge from one to the other.

**The concurrency bug this section explicitly flags lives right here.** If `User.create()` on the previous line succeeded, but *this* call — whichever of `Client.create()` or `Lawyer.create()` applies — then throws for any reason at all (a validation failure, a dropped database connection, anything), there is now a `User` document sitting permanently in the database with **no matching profile document at all**. Nothing anywhere in this function reaches back and undoes the earlier `User.create()` — there is no automatic rollback. The correct, complete fix — which the file's own design notes explicitly mark as future work rather than something already solved — is a **MongoDB transaction**: a mechanism that lets multiple writes spanning multiple collections either *all* succeed together, or *all* roll back together, with no possibility of the database ever being caught in a visible half-finished state in between.

```js
await addRefreshSession(user, refreshToken, req.headers['user-agent']);
```

This one line demonstrates a small but genuinely important JavaScript syntax rule: **bracket notation, used here because the property name has a hyphen in it.** Dot notation (`req.headers.something`) only works when the property name you're accessing is also a legal JavaScript variable name — and hyphens are not legal inside variable names, because a hyphen is the subtraction operator. If this code had been written as `req.headers.user-agent`, JavaScript would actually try to parse that as "take the value at `req.headers.user`, then subtract something named `agent`" — nonsense in this context, and it would either throw an error or silently produce something completely wrong. Bracket notation, `req.headers['user-agent']`, sidesteps the whole problem entirely by treating the whole thing as a plain string used as a key, rather than as literal JavaScript syntax to be parsed. Any HTTP header containing a hyphen — and there are many common ones, like `content-type` or `x-forwarded-for` — needs exactly this same bracket treatment. This is also a small but genuine networking detail worth knowing on its own: HTTP header names very commonly contain hyphens as a matter of convention, which is precisely why Express exposes all headers as a plain object you index into with brackets, rather than something you'd naturally dot into.

```js
} catch (err) {
  return res.status(500).json({ message: "Registration failed.", error: err.message });
}
```

This is a manually written `try/catch`, rather than a cleaner, more reusable pattern you might see in a more mature codebase — something like a shared `asyncHandler` wrapper combined with a custom `AppError` class that centralizes how errors get turned into responses. There's a genuine security concern worth flagging in this specific line: `error: err.message` sends whatever the raw, internal error message says directly back to whoever made the request. In the ordinary case, this is completely harmless — usually it's something benign like "email already exists." But if the underlying error came from somewhere less expected — say, a raw database driver failure, or some unrelated internal exception — its message could end up describing internal implementation details (database names, internal file paths, library internals) that have no business being visible to an outside client at all. In a cleaner rewrite, this catch block should ideally shrink down to simply re-throwing the error and letting one single, centralized, shared error handler elsewhere in the app decide, in one place, what's actually safe to reveal externally.

### Worked example

**Input** (an HTTP POST request body):

```json
{
  "name": "Amara Chen",
  "email": "amara@example.com",
  "phone": "+15551234567",
  "password": "correcthorsebatterystaple",
  "role": "lawyer"
}
```

**Output on success** — a new `User` document is created (with a bcrypt password hash, never the plain password), a matching `Lawyer` document is created, a refresh session is recorded, and the HTTP response sent back to the client is something like:

```json
{
  "accessToken": "eyJhbGciOi...(a signAccessToken output)...",
  "refreshToken": "eyJhbGciOi...(a signRefreshToken output)...",
  "user": { "id": "64f1a2b3c4d5e6f7a8b9c0d1", "name": "Amara Chen", "role": "lawyer" }
}
```

**Output on a validation failure** — same request, but `"role": "admin"` instead:

```json
{ "message": "Invalid role." }
```

sent with HTTP status `400`.

**Output on a duplicate email** — same request, but this email was already registered by someone else:

```json
{ "message": "Email or phone already registered." }
```

sent with HTTP status `409`.

* * *

## 9\. `login` — checking a claim of identity against stored credentials

```js
const user = await User.findOne({ email }).select('+passwordHash');
```

To understand `.select('+passwordHash')`, you first need to know something about how the `User` schema is defined elsewhere in the app: it marks the `passwordHash` field with `select: false`. That setting means, **by default, every single query against the** `User` **model anywhere in this entire codebase** — `User.find()`, `User.findById()`, all of them, automatically leaves `passwordHash` out of whatever it returns, with zero extra effort required from whoever happens to be writing that particular query. This is a strong, safe *default*: some completely unrelated controller elsewhere in the app, written by someone who has never thought about password security at all, can freely query users for, say, a public profile listing, and it is simply *impossible* for that query to accidentally leak a password hash into an API response — because the field was never even fetched from the database in the first place, let alone serialized into JSON.

`.select('+passwordHash')` is the deliberate, explicit override to that default. The `+` prefix specifically means "yes, I am aware this field is normally hidden, and I specifically want it included, right here, right now, because this particular piece of code genuinely needs it." The practical benefit: it forces every single place in the codebase that actually needs to compare passwords to say so explicitly, right at that exact call site — rather than password access being some ambient possibility lurking everywhere, invisible until something goes wrong.

```js
if (!user || !user.isActive) {
  return res.status(401).json({ message: "Invalid credentials." });
}
```

This line is about **preventing user enumeration**. Notice: "no user with this email exists at all" and "a user with this email exists, but their account has been deactivated" produce the *exact same* outcome — same status code (`401`), same message (`"Invalid credentials."`). Consider what would happen if these two cases produced differently worded responses instead — say, `"No account found"` versus `"Account disabled"`. An attacker could then send login attempts for a huge list of email addresses they're curious about, never even attempting a password, and use *only the wording difference in the response* to silently build up a list of which of those email addresses correspond to real, registered accounts on this system — a genuine privacy and security leak, achieved without ever guessing a single password. Making the two outcomes indistinguishable from the outside closes that door entirely.

```js
const isMatch = await bcrypt.compare(password, user.passwordHash);
if (!isMatch) {
  return res.status(401).json({ message: "Invalid credentials." });
}
```

`bcrypt.compare(plaintext, storedHash)` is the counterpart to `bcrypt.hash` from section 8. Internally, it reads the salt that's embedded directly inside `storedHash` (recall: bcrypt bakes the salt right into its own output string), redoes the exact same hashing computation on the freshly submitted `plaintext` password using that same extracted salt, and then checks whether the two results match. This comparison is already timing-safe *internally*, by construction — which is exactly why this file doesn't need to wrap it in `timingSafeEqual` the way `refreshTokenMatchesDigest` had to in section 6; that protection is already built into `bcrypt.compare` itself.

Now look closely at the exact wording of this failure message compared to the one two lines above: **identical**, character for character — `"Invalid credentials."`, both times, with the same `401` status both times. This is the second half of the anti-enumeration design started above: whether the underlying reason is "no such email exists at all," or "the account exists but is deactivated," or "the password submitted is simply wrong," an attacker probing this endpoint receives back the exact same signal every single time, with absolutely no way to distinguish any of those three genuinely different situations from the outside.

### Worked example

**Input (correct credentials, active account):**

```json
{ "email": "amara@example.com", "password": "correcthorsebatterystaple" }
```

**Output:**

```json
{
  "accessToken": "eyJhbGciOi...",
  "refreshToken": "eyJhbGciOi...",
  "user": { "id": "64f1a2b3c4d5e6f7a8b9c0d1", "name": "Amara Chen", "role": "lawyer" }
}
```

**Input (an email that was never registered):**

```json
{ "email": "nobody-registered-this@example.com", "password": "anything" }
```

**Output** (status `401`):

```json
{ "message": "Invalid credentials." }
```

**Input (correct email, wrong password):**

```json
{ "email": "amara@example.com", "password": "wrongGuessHere" }
```

**Output** (status `401` — indistinguishable from the previous case):

```json
{ "message": "Invalid credentials." }
```

**Input (correct email and password, but the account was deactivated by an admin):**

**Output** (status `401` — again, indistinguishable):

```json
{ "message": "Invalid credentials." }
```

* * *

## 10\. `refreshToken` — the most intricate handler in the file

```js
let payload;
try {
  payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
} catch {
  return res.status(401).json({ message: "Invalid or expired refresh token." });
}
```

`jwt.verify(token, secret)` performs two distinct checks in one call, and either one failing causes the whole call to throw an exception rather than return some kind of "false" value: first, is the signature actually valid — was this token really signed using `JWT_REFRESH_SECRET`, and has it not been tampered with since? Second — using the same `exp`/`Date.now()` mechanism first described back in section 2 — has the token's expiry timestamp already passed? If either check fails for any reason, `jwt.verify` **throws**, and that's exactly why this call needs its own dedicated `try/catch`, separate from any error handling elsewhere in the app.

`catch { ... }` — with nothing at all inside the parentheses — is called **optional catch binding**. The normal, more familiar form is `catch (err) { ... }`, which lets you inspect exactly what went wrong inside the `err` variable. Here, the code deliberately doesn't do that, because it genuinely does not care *why* verification failed — an expired token, a tampered token, a completely malformed garbage string — all of them are treated identically, as "reject this." Since the specific error is never going to be looked at, there's no reason to even bother naming a variable for it.

```js
const user = await User.findById(payload.userId).select("+refreshTokens.tokenHash");
```

It's important to be precise about what passing `jwt.verify` actually proves, and what it does *not* prove. It proves the token's signature is legitimate, and that the token hasn't expired yet. It does **not** prove that the token hasn't been *revoked* — for example, that the user hasn't already logged out on that device (section 11), or that this exact token hasn't already been used once already as part of the rotation mechanism you're about to read below. That distinction is exactly why the very next step reaches into the database to fetch this user's actual, current, mutable list of active sessions to cross-check against: a cryptographic signature is a fixed mathematical fact about a string that, once true, stays true forever — it can never be "un-issued" after the fact purely by its own logic. Making revocation possible at all requires checking against something *external* and *mutable* that lives outside the token itself — which is precisely what this database lookup provides.

```js
const oldTokenHash = digestRefreshToken(refreshToken);
const hasMatchingSession = user.refreshTokens.some((session) => (
  refreshTokenMatchesDigest(refreshToken, session.tokenHash)
));
```

`.some()` is a built-in array method that walks through `user.refreshTokens` one entry at a time and, for each `session`, runs the given function. The very first time that function returns `true` for any element, `.some()` **stops immediately** and returns `true` for the whole call, without bothering to check any of the remaining array elements — this is called **short-circuit evaluation**, the same underlying idea you already saw at work in the `device || 'unknown'` fallback back in section 7, just applied here to loop control instead of a single expression. Every individual comparison performed inside this loop goes through `refreshTokenMatchesDigest` from section 6 — which means every single comparison happening here, not just some overall final result, is timing-safe.

This whole check is the actual mechanism that makes revocation genuinely work, and it's worth restating plainly: a JWT's signature being valid is a permanent, unchangeable mathematical fact — a signed token has no built-in concept of "and by the way, I've since been logged out." The *only* way to make a previously-valid, unexpired token stop working is to check it against something outside the token entirely — here, the current, mutable list of session hashes stored on this specific user's document. If `logout` (section 11) has already removed this particular session's hash from that array, `.some()` finds no matching entry anywhere in the list, `hasMatchingSession` becomes `false`, and this otherwise perfectly valid, correctly-signed, not-yet-expired token gets rejected anyway. This is the entire reason this file goes to the trouble of storing hashed sessions server-side in the first place, rather than simply trusting JWTs as fully "stateless" tokens the way some simpler designs do.

```js
const newTokenHash = digestRefreshToken(newRefreshToken);
const rotation = await User.updateOne(
  { _id: user._id, isActive: true, 'refreshTokens.tokenHash': oldTokenHash },
  { $set: { 'refreshTokens.$[session].tokenHash': newTokenHash } },
  { arrayFilters: [{ 'session.tokenHash': oldTokenHash }] }
);
if (rotation.modifiedCount !== 1) {
  return res.status(401).json({ message: 'Refresh token was already used or revoked.' });
}
```

This is the single most important concurrency mechanism in the entire file — worth reading slower than anything else here.

**Look carefully at the filter object first:** `{ _id: user._id, isActive: true, 'refreshTokens.tokenHash': oldTokenHash }`. Focus on that third condition specifically: it says "only match this document if the *old* hash is *still currently present* somewhere in the array, right at this exact instant, at the moment this write is actually executing." This is the same core idea as `addRefreshSession`'s filter back in section 7 — folding a condition directly into the atomic write itself, rather than checking it separately beforehand — but here it's being used to do something more powerful. This specific pattern has a name: **optimistic concurrency control**, sometimes also called **compare-and-swap**: "update this field, but *only if* it still currently has the exact value I expect it to have; if it doesn't, do nothing at all."

**Here's the exact replay attack this single mechanism defends against.** Imagine an attacker somehow steals a valid, not-yet-expired refresh token — say, through a compromised browser extension sitting on the victim's machine. At some point afterward, purely by unlucky timing, both the legitimate user (using their phone, say) and the attacker (using the stolen token) happen to send a refresh request using that *exact same* token, at nearly the same instant — a genuine, real race condition, not a hypothetical one. Both of these requests pass every single check described above: both signatures verify fine, neither token has expired, and both find a matching session in the array via `.some()`. But only *one* of them can possibly win at *this specific line*. Whichever `updateOne` call happens to physically reach MongoDB first finds `oldTokenHash` still present in the filter, and succeeds — `rotation.modifiedCount` becomes `1`, and the stored hash gets replaced with `newTokenHash`. The instant that happens, the *second* racing request's filter — which is still looking for the *old* hash — no longer matches anything at all, because the underlying document has already changed out from under it, milliseconds earlier. Its `modifiedCount` comes back `0`, and it gets correctly rejected with "already used or revoked." **The atomicity of a single MongoDB write guarantees that only one of the two racing requests can ever win — there is no possible outcome where both succeed.**

`arrayFilters` **is the piece that makes the actual field update precise enough to work at all.** `refreshTokens` is an array, and any given user could have up to 10 separate session subdocuments sitting inside it at once (thanks to `MAX_REFRESH_SESSIONS` from section 2). The path `'refreshTokens.$[session].tokenHash'` uses a special positional placeholder, `$[session]`, which reads as "whichever specific array element the `arrayFilters` option below identifies as the one named `session`." And `arrayFilters: [{ 'session.tokenHash': oldTokenHash }]` is exactly what defines that: "the one particular array element whose `tokenHash` field equals `oldTokenHash`." Without this mechanism, MongoDB would have no way at all to know *which one* of up to ten array entries you actually meant to update — you'd need some other, clunkier approach to target one specific element inside an array.

### Worked example

**Input** (client sends its current refresh token, along with a freshly generated `newRefreshToken` the server has already produced internally for this request):

```js
const refreshToken = "eyJhbGciOi...(the client's existing, currently-stored token)...";
// newRefreshToken was generated earlier in the handler via signRefreshToken(user)
```

**Output on success** — the old session's stored hash is atomically replaced with the new one, and the client receives:

```json
{
  "accessToken": "eyJhbGciOi...(a brand-new signAccessToken output)...",
  "refreshToken": "eyJhbGciOi...(newRefreshToken)..."
}
```

**Output if the exact same (now-stale) refresh token is presented a second time** — for example, by a browser tab that cached the old token and tried to reuse it after rotation already happened — status `401`:

```json
{ "message": "Refresh token was already used or revoked." }
```

**Output if the token has simply expired (30+ days old)** — status `401`:

```json
{ "message": "Invalid or expired refresh token." }
```

* * *

## 11\. `logout` — deliberately forgiving, on purpose

```js
try {
  payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
} catch {
  return res.status(200).json({ message: "Logged out successfully." });
}
```

This has the exact same shape as the `try`/`catch` in `refreshToken` above — same `jwt.verify` call, same empty `catch` — but notice the status code on failure is completely different: **200 here, versus 401 there.** This difference is a deliberate design choice about user experience combined with security reasoning, not an inconsistency to fix. Think about what someone hitting the logout endpoint actually *wants*: exactly one outcome — to no longer be logged in. If it turns out their token was already garbage, already expired, or was never even valid in the first place, then the end state they were actually asking for — "I am now logged out" — is *already true*, trivially. Treating that situation as an *error* would be actively misleading to the caller; nothing needs fixing, nothing went wrong from their point of view, and there's no useful corrective action for them to take. This specific behavior — doing the same operation on an already-invalid or already-applied input and getting the same successful result every time, with no error — is called **idempotent logout** (more generally, an idempotent operation is one where doing it once, or doing it many times, produces the same end state).

```js
await User.updateOne(
  { _id: payload.userId },
  { $pull: { refreshTokens: { tokenHash: digestRefreshToken(refreshToken) } } }
);
```

`$pull` is a MongoDB update operator that removes every array element matching a given condition — here, specifically the one subdocument (if any) whose `tokenHash` matches the digest of exactly this one token being logged out with. Because it's scoped to that one exact hash, and not to "clear the whole array," logging out on one device — a phone, say — leaves up to nine other possible active sessions (a laptop, a tablet, a different browser) **completely untouched**. This is exactly the behavior you'd want in a real multi-device app: logging out should end the session you're actually using right now, not silently sign the user out of every device they own everywhere else.

### Worked example

**Input (a currently valid, active refresh token):**

```json
{ "refreshToken": "eyJhbGciOi...(a live session's token)..." }
```

**Output** (status `200`) — this session's entry is removed from `refreshTokens`, all other sessions untouched:

```json
{ "message": "Logged out successfully." }
```

**Input (a garbage string, or an already-expired token):**

```json
{ "refreshToken": "not-a-real-token-at-all" }
```

**Output** (still status `200`, same message — no error surfaced to the caller):

```json
{ "message": "Logged out successfully." }
```

* * *

## 12\. The Postman test table (reference)

Look at the *shape* of this table before the specific rows, because the organizing idea is the actual lesson here, more than any one specific test case: the tests are grouped by **category of scenario** — *(happy path)*, *(validation)*, *(auth failure)*, *(not found/revoked)*, *(edge case: role tampering)*, *(edge case: duplicate)*, *(rotation replay)*, *(logout idempotency)*. A genuinely thorough test plan for **any** authentication system, not just this one, should aim to cover that same *shape* of ground: does the ideal, everything-goes-right path actually work; is obviously bad input rejected early, before it can cause any damage; are wrong credentials rejected without ever leaking information about *why* they were wrong; are already-used, already-expired, or already-revoked tokens correctly caught and rejected; can someone bypass an intended whitelist by simply sending an unexpected raw value straight to the API; can the same resource accidentally end up created twice; can a token meant to be used exactly once actually be used twice; and does "undo" behavior, like logging out, still behave sensibly even when handed already-broken input. Test #7, labeled "rotation replay," directly exercises the atomicity mechanism from section 10 — reusing a token from *before* a rotation happened should fail specifically because that token's hash no longer exists anywhere in the array, having already been overwritten by the rotation.

* * *

## 13\. Reading the `refreshToken` flowchart as a decision tree

Every diamond-shaped decision point in that flowchart corresponds to one real, specific `if` statement or filter condition inside section 10's code — nothing in the diagram is decorative:

*   "refreshToken present?" → the very first input-validation check, before anything else runs.
    
*   "Does `jwt.verify` throw?" → the `try/catch` wrapped around the verification call.
    
*   "Not found, or `!isActive`?" → the check immediately after `User.findById`.
    
*   "No matching session found?" (via `.some()`) → the revocation check.
    
*   "`modifiedCount !== 1`?" → the atomic, compare-and-swap rotation check.
    

The value of actually drawing a chart like this out **before** writing a handler (or while trying to make sense of one someone else already wrote) is that it forces every branch to become explicit, on paper, including the one branch that's genuinely easy to forget entirely: *"what if the token is syntactically perfect, correctly signed, and not yet expired — but was already logged out?"* That branch simply doesn't exist at all in a naive JWT implementation that trusts tokens as purely "stateless" proof — it exists here *only* because of the deliberate session-hash-checking design built out across every function in section 10.

* * *

## Self-check before moving on

1.  Why does `signRefreshToken` deliberately leave `role` out of its payload, while `signAccessToken` includes it? What's the worst-case consequence if that were reversed?
    
2.  Why is `digestRefreshToken` deterministic while `bcrypt.hash` is deliberately not — and for each one, why does that specific property matter for the specific job it does?
    
3.  Walk through, step by step, exactly why two simultaneous requests using the same stolen refresh token can never *both* succeed inside `refreshToken`.
    
4.  Why does `login` return the literal same error message and status code for "no such user exists" and "the password was simply wrong"?
    
5.  Why does `addRefreshSession`'s database filter include `isActive: true` directly inside it, instead of checking `isActive` earlier in the code and simply trusting that earlier result?
    
6.  Trace where in this file a plain *string* like `'15m'` actually gets turned into a real numeric timestamp — and then trace where, later, that same timestamp gets read and checked again.
