Backend from First Principles

Topic 4 of 4

Auth & Authorization

Authentication vs Authorization

Interview answer

Authentication checks who you are (email + password). Authorization checks what you are allowed to do (your role). Authentication always comes first. Not logged in → 401. Logged in but not allowed → 403.

Authentication first, then authorization

Ali is a user

AliServerDatabase

Ali is an admin

AliServerDatabase

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Ali is a user
Step through the diagram to see requests and responses.
Ali is an admin
Step through the diagram to see requests and responses.

Two separate checks happen on the request:

  1. Who is this? The server checks the password or token → "this is Ali".
  2. Can Ali do this? The server looks at Ali's role → user can't delete users, admin can.

The password problem

Interview answer

Before OAuth, if a travel app needed your Gmail contacts, it asked for your Gmail password. That gave it full access to everything, and the only way to take it back was to change your password. OAuth fixed this with tokens that have limited permissions.

The delegation problem

Without OAuth

YouTravel AppGmailTravel App holds—

With OAuth

YouTravel AppGoogleTravel App holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Without OAuth
Step through the diagram to see requests and responses.
With OAuth
Step through the diagram to see requests and responses.

Why handing over your password was a disaster:

  • The app can read, send and delete all your email.
  • To take access back, you change your password, and every other app breaks.
  • Same password on other sites? One leak exposes all of them.

How auth evolved: Passwords → MFA → OAuth → JWT → Zero Trust → Passkeys.

Password hashing

Interview answer

I never store the password itself. I store an Argon2 hash of it. At login, I verify what the user typed against that hash. Argon2 is slow on purpose, so if the database leaks, attackers can't guess passwords fast, the way they can with MD5 or SHA-256.

What a database leak exposes

Plain text

ServerDatabaseAttackerDatabase holds—Attacker holds—

Argon2 hash

ServerDatabaseAttackerDatabase holds—Attacker holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Plain text
Step through the diagram to see requests and responses.
Argon2 hash
Step through the diagram to see requests and responses.

Why not MD5 or SHA-256? They are built to be fast, so a GPU can try billions of guesses per second. Argon2 is built to be slow and to need a lot of memory for every guess.

Salt: Argon2 mixes a random value into each password before hashing, so two users with the same password get different hashes.

js
const argon2 = require('argon2')

// REGISTER: hash and store (salt + settings are saved inside the hash string)
const hash = await argon2.hash(password)
await pool.query('INSERT INTO users (email, password) VALUES ($1, $2)', [email, hash])

// LOGIN: verify
const valid = await argon2.verify(user.password, password)
if (!valid) throw new AuthError('authentication failed')

MFA: multi-factor authentication

Interview answer

MFA means proving who you are with two different types of proof: something you know (password), something you have (phone), or something you are (fingerprint). If someone steals the password, they still can't log in without the second one.

Login with password + authenticator code
Phoneauthenticator appAliServerPhone holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
FactorTypeExample
Something you knowKnowledgePassword, PIN
Something you havePossessionPhone, authenticator app
Something you areInherenceFingerprint, Face ID

Passkeys (WebAuthn)

Interview answer

A passkey is a key pair on your device. The server keeps only the public key. At login the server sends a random challenge, the device signs it after Face ID, and the server checks the signature with the public key. No password is ever sent, so there's nothing to phish or leak.

Passkey registration and login
Devicephone / laptopBrowserServerDevice holds—Server holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
  • The private key never leaves your phone or laptop.
  • The server stores only the public key, which is useless to an attacker.
  • The passkey only works on the real site (notes.app), so a fake site gets nothing.

HTTP is stateless

Interview answer

HTTP doesn't remember anything between requests; every request arrives like a stranger. So after login, the client must send proof on every request: either a session ID (the server remembers the user) or a JWT (the token itself says who the user is).

The server forgets you right after login
BrowserServer

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.

Two ways to carry that proof:

  • Session (stateful): the server saves you in Redis; the browser keeps only an ID.
  • JWT (stateless): the token itself carries who you are.

Session-based authentication

Interview answer

At login the server creates a random session ID, saves sessionId → userId in Redis, and sends the ID to the browser in a cookie. On every request the browser sends the cookie back and the server looks it up in Redis. Logout = delete the key, so access is gone instantly.

Sessions across multiple servers

Session in server memory

BrowserServer 1Server 2Server 1 holds—Server 2 holds—

Shared Redis

BrowserServer 1Server 2RedisRedis holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Session in server memory
Step through the diagram to see requests and responses.
Shared Redis
Step through the diagram to see requests and responses.
  1. Login → server makes a random ID abc123.
  2. Redis stores abc123 → { userId: 42 }.
  3. Browser gets cookie sessionId=abc123.
  4. Every request sends the cookie → Redis lookup → "user 42".

The diagram above shows why Redis must be shared when you run more than one server.

js
// LOGIN (after verifying the password)
const sessionId = crypto.randomUUID()
await redis.setex(`session:${sessionId}`, 1800, JSON.stringify({ userId: user.id, role: user.role }))
res.cookie('sessionId', sessionId, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 1800000 })

// PROTECTED ROUTE
const data = await redis.get(`session:${req.cookies.sessionId}`)
if (!data) return res.status(401).json({ error: 'authentication failed' })

// LOGOUT: instant revocation
await redis.del(`session:${req.cookies.sessionId}`)

JWT: JSON Web Tokens

Interview answer

A JWT is a signed token with three parts: header, payload, signature. The payload holds the user ID, role and expiry. The server checks the signature with its secret, with no database lookup, which is why it's called stateless. The payload is only encoded, not encrypted, so anyone can read it: never put secrets in it.

JWT decoder

⚠ Anyone holding this token can read the payload below. No key was needed to decode it. It is encoded, not encrypted.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzk5OTk5OTk5fQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header
{
  "alg": "HS256",
  "typ": "JWT"
}
Payload
{
  "sub": "42",
  "role": "user",
  "exp": 1799999999
}
SignatureSflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Not decoded: it's raw bytes from an HMAC over header.payload with a shared secret. This tool does not verify it. Decoding ≠ verifying.

  • alg: HS256 — symmetric: whoever can verify can also sign.
  • exp: 2027-01-15 07:59:59 UTC

Try to tamper with a JWT

The server signed this token for Ali (role: user). You're the attacker: you can read and edit the payload, but you don't know the server's secret.

HEADER.PAYLOAD.SIGNATURE

  • Header: which algorithm signed it, e.g. HS256.
  • Payload: sub (user ID), role, exp (expiry time).
  • Signature: HMAC-SHA256(header + "." + payload, SECRET). Change one letter of the payload and it no longer matches.
ProsCons
No session storageHard to revoke before it expires
Works across many serversA stolen token works until it expires
Great for microservicesAnyone can read the payload
Portable between servicesTokens can get large

Many services: with HS256, every service needs the same secret to verify, so every service could also create tokens. With RS256, only the auth service has the private key to sign; the others only have the public key to verify.

js
// LOGIN: create a JWT
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' })

// MIDDLEWARE: verify on every protected request
function authenticate(req, res, next) {
  try {
    const token = req.headers.authorization.split(' ')[1] // "Bearer <token>"
    req.user = jwt.verify(token, process.env.JWT_SECRET)
  } catch {
    return res.status(401).json({ error: 'authentication failed' })
  }
  next()
}

Sessions vs JWT

Interview answer

With sessions, the server stores the state in Redis and the client only carries an ID, so it's easy to revoke but needs a lookup on every request. With JWT, the token carries the state, so there's no lookup and it scales easily, but you can't cancel it before it expires.

Where does state live on each request?

Session (stateful)

BrowserServerRedisBrowser holds—Redis holds—

JWT (stateless)

ClientServerNo store(nothing here)Client holds—No store holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Session (stateful)
Step through the diagram to see requests and responses.
JWT (stateless)
Step through the diagram to see requests and responses.
PropertySession (stateful)JWT (stateless)
Server stores stateYes (Redis)No
Client carriesSession ID onlyFull token with claims
Revoke immediatelyYes: delete from RedisHard: needs a blocklist
Distributed systemsNeeds shared RedisAny server verifies locally
MicroservicesComplicatedNatural fit
Token theftDelete the sessionValid until expiry
Best forWeb apps, single regionAPIs, microservices, distributed
InfrastructureRedis requiredJust a key

Hybrid: short JWT + refresh token

Interview answer

In production I use both: a short access JWT (15 min) for fast requests with no lookup, and a long refresh token (30 days) in Redis for control. When the JWT expires, the refresh token gets a new JWT and is itself replaced (rotation). If an already-used refresh token shows up again, it was stolen, so I revoke the whole token family.

Refresh token rotation with reuse detection
AttackerAli's appAPI serverRedisAttacker holds—Ali's app holds—Redis holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
  • Normal request: only the JWT, checked in memory.
  • JWT expired: send the refresh token → Redis check → new JWT + new refresh token.
  • An old refresh token shows up again: someone copied it → revoke every token from that login (the family).
  • Logout: revoke the refresh token; the JWT dies within 15 minutes.
js
async function refresh(token) {
  const record = JSON.parse(await redis.get(`rt:${token}`)) // { userId, role, familyId }
  if (!record || (await redis.exists(`revoked:${record.familyId}`))) throw new AuthError('invalid')

  // mark as used; NX fails if it was already used -> reuse detected
  const firstUse = await redis.set(`used:${token}`, 1, 'EX', THIRTY_DAYS, 'NX')
  if (!firstUse) {
    await redis.set(`revoked:${record.familyId}`, 1, 'EX', THIRTY_DAYS) // revoke whole family
    throw new AuthError('invalid')
  }

  const newToken = crypto.randomUUID()
  await redis.set(`rt:${newToken}`, JSON.stringify(record), 'EX', THIRTY_DAYS) // same familyId
  const accessToken = jwt.sign({ sub: record.userId, role: record.role }, SECRET, { expiresIn: '15m' })
  return { accessToken, refreshToken: newToken }
}

Cookies

Interview answer

A cookie is not an auth method. It's a small piece of browser storage that is sent automatically with every request to that site. It can carry a session ID or a JWT. I protect it with HttpOnly (JavaScript can't read it), Secure (HTTPS only) and SameSite (not sent on cross-site requests).

Cookie playground

Set-Cookie: sessionId=abc123; SameSite=Lax; Secure; HttpOnly
SameSite

An XSS script injected into notes.app runs:

> document.cookie
""

✓ HttpOnly: JavaScript can't read the cookie, so the script can't steal it.

A cookie can carry a session ID or a JWT. The mechanism is the session or the JWT; the cookie just carries it.

AttributeWhat it does
HttpOnlyJavaScript can't read it. Protects against XSS stealing the token.
SecureOnly sent over HTTPS.
SameSite=LaxNot sent on cross-site POSTs or background requests; sent on normal link clicks. Reduces CSRF.
SameSite=StrictNever sent on cross-site requests. Strictest CSRF protection.
MaxAgeHow long the cookie lives.

Cookie vs Authorization header

  • Cookie (browser apps): sent automatically. HttpOnly protects against XSS, and SameSite against CSRF.
  • Authorization header (mobile, machine to machine): Authorization: Bearer eyJ…, set manually. There's no CSRF risk because it's never sent automatically.
js
res.cookie('sessionId', sessionId, {
  httpOnly: true, // JS can't read it
  secure: true, // HTTPS only
  sameSite: 'lax', // CSRF protection
  maxAge: 1800000, // 30 minutes
})

CSRF tokens

Interview answer

CSRF is when another website makes your browser send a request to my site, and the browser automatically attaches your cookie. The fix is a random CSRF token that my own frontend sends in a header. The other site can't read that token, so its fake request gets 403.

Cross-site request forgery

No CSRF token

evil.comAli's browsernotes.appnotes.app holds—

With CSRF token

evil.comAli's browsernotes.appnotes.app holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

No CSRF token
Step through the diagram to see requests and responses.
With CSRF token
Step through the diagram to see requests and responses.

evil.com can make your browser send a request, but it can't read notes.app's pages, so it never learns the token.

SameSite=Lax already blocks most of these; the token is a second lock. Never change data with a GET request.

js
// at login: a random token tied to the session
req.session.csrfToken = crypto.randomBytes(32).toString('hex')

// frontend: send it back on every POST / PUT / PATCH / DELETE
fetch('/notes/7/delete', { method: 'POST', headers: { 'X-CSRF-Token': csrfToken } })

// middleware
function csrf(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next()
  if (req.get('X-CSRF-Token') !== req.session.csrfToken) {
    return res.status(403).json({ error: 'invalid csrf token' })
  }
  next()
}

OAuth 1.0 and OAuth 2.0

Interview answer

OAuth lets an app use part of my data, like my Google contacts, without my password. Google asks me to approve, then gives the app a token with only that permission, and I can revoke it any time. OAuth 2.0 is only about what the app can access, not who I am; that part is OIDC.

OAuth 1.0 solved delegation: the travel app redirects you to Google, you approve "read contacts," and the app gets a token for contacts only. You never gave your password, and you can revoke the token anytime. But it needed complex cryptographic signatures on every request.

OAuth 2.0 simplified it with bearer tokens (no per-request signatures) and different flows:

FlowWhen to use
Authorization Code (+ PKCE)Web, mobile and browser apps: the most common and most secure
Client CredentialsMachine to machine, no user involved
Device CodeTVs, CLIs, devices with no browser
ImplicitOld and deprecated; replaced by Authorization Code + PKCE
ComponentWhat it is
Resource OwnerYou, the user
ClientThe app that wants access (travel app)
Resource ServerThe server holding your data (Gmail)
Authorization ServerThe server that issues tokens (Google auth)

Bearer token: whoever bears (carries) the token gets access. The server doesn't check who is holding it, only that it's valid. That's why a stolen bearer token is dangerous.

PKCE

Interview answer

PKCE protects the login code. Before redirecting, the app makes a random secret (the code_verifier) and sends only its hash to Google. When it exchanges the code for tokens, it must send the original secret. So a stolen code alone is useless.

Stolen authorization code: without vs with PKCE

Without PKCE

Notes appGoogleAttackerAttacker holds—

With PKCE

Notes appGoogleAttackerNotes app holds—Attacker holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Without PKCE
Step through the diagram to see requests and responses.
With PKCE
Step through the diagram to see requests and responses.

Mobile and browser apps can't hide a client secret (anyone can open the app's code), so PKCE gives them a fresh secret for every single login.

js
const verifier = crypto.randomBytes(32).toString('base64url')                  // keep secret
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')

// 1. redirect: /authorize?...&code_challenge=${challenge}&code_challenge_method=S256
// 2. exchange: POST /token ... &code=${code}&code_verifier=${verifier}

OpenID Connect (OIDC)

Interview answer

OIDC is OAuth 2.0 plus identity. Besides the access token, Google gives an ID token, a JWT that says who the user is; that's what "Sign in with Google" uses. Google first redirects back with only a code, and my server exchanges that code for the ID token and access token.

Sign in with Google (Authorization Code flow)
BrowserAliNotes appserverGoogleauth serverGoogle APIsBrowser holds—Notes app holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
  • Access token → what the app can do (call Google APIs).
  • ID token → who the user is (name, email). It's a JWT that your app checks:
json
{
  "sub": "google-user-123",
  "name": "Ali Ahmed",
  "email": "ali@gmail.com",
  "iss": "https://accounts.google.com",
  "aud": "your-client-id",
  "iat": 1700000000,
  "exp": 1700003600
}

SSO and SAML

Interview answer

SSO means logging in once at a central login server, the identity provider (like Okta), and then every company app lets you in without asking again. SAML is the older XML way to do it; OIDC is the newer JSON/JWT way.

SSO with SAML: log in once, use many apps
BrowserAliNotes appSPIdPOkta / EntraWiki appSPBrowser holds—IdP holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
SAMLOIDC
FormatXML assertionJSON / JWT (ID token)
Age2005, enterprise2014, modern
Typical useCorporate SSO (Okta, Entra ID, ADFS)"Sign in with Google", mobile, SPAs
Signed byIdP's private key (XML signature)IdP's private key (JWT signature)

API key authentication

Interview answer

An API key is a long secret string that a program sends with every request, in a header. There's no login and no session; the server looks the key up to know which client it is. It's for machine-to-machine calls. I store only a hash of the key, like a password.

One request with an API key
Clientscript / serverAPIDatabase

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
Use caseBest authWhy
Web app loginSession or JWTA user logs in
Mobile appJWTStateless, portable
Machine to machineAPI keyNo user involved
Third-party accessOAuth 2.0Limited, revocable permissions
Sign in with GoogleOIDCIdentity from Google
js
async function apiKeyAuth(req, res, next) {
  const key = req.headers['x-api-key']
  if (!key) return res.status(401).json({ error: 'authentication failed' })

  // store and look up a HASH of the key, never the raw key
  const hash = crypto.createHash('sha256').update(key).digest('hex')
  const { rows } = await pool.query('SELECT user_id FROM api_keys WHERE key_hash = $1 AND active = true', [hash])
  if (!rows[0]) return res.status(401).json({ error: 'authentication failed' })

  req.user = { id: rows[0].user_id }
  next()
}

Service-to-service auth (mTLS)

Interview answer

In normal HTTPS, only the server shows a certificate. In mutual TLS, both sides show one, so two services can prove who they are to each other without API keys. In Kubernetes, a service mesh like Istio sets this up automatically.

Normal TLS vs mutual TLS between two services

Normal TLS

ordersservicepaymentsservice

Mutual TLS

ordersservicepaymentsservice

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Normal TLS
Step through the diagram to see requests and responses.
Mutual TLS
Step through the diagram to see requests and responses.
js
const https = require('https')

https.createServer({
  key: fs.readFileSync('payments.key'),
  cert: fs.readFileSync('payments.crt'),
  ca: fs.readFileSync('internal-ca.crt'), // trust only our internal CA
  requestCert: true, // ask the caller for a certificate
  rejectUnauthorized: true, // no valid certificate → handshake fails
}, app).listen(443)

app.post('/charge', (req, res) => {
  const caller = req.socket.getPeerCertificate().subject.CN // "orders.internal"
  if (caller !== 'orders.internal') return res.status(403).end()
  // ...charge
})

RBAC: role-based access control

Interview answer

RBAC means each user has a role (user, moderator, admin) and each role has permissions. A middleware checks the role on every request and returns 403 if it's not allowed. Before that, authentication must return 401 for a bad token, not crash with 500.

RBAC request simulator

Token
Role in token
Route
app.delete('/notes/:id', authenticate, authorize('admin', 'moderator'), deleteNote)
RequestDELETE /notes/:id
authenticateverify JWT
authorizerole: user
handlerdeleteNote
…

Pick a token, role and route, then send the request.

  • user → read own notes
  • moderator → read and write any note
  • admin → everything, plus the dead zone
js
// authentication: who are you? (jwt.verify throws on a bad token, so catch it -> 401)
function authenticate(req, res, next) {
  try {
    const token = req.headers.authorization.split(' ')[1]
    req.user = jwt.verify(token, process.env.JWT_SECRET)
  } catch {
    return res.status(401).json({ error: 'authentication failed' })
  }
  next()
}

// authorization: what can you do?
function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'forbidden' })
    next()
  }
}

app.get('/admin/dead-zone', authenticate, authorize('admin'), getDeadZoneNotes)
app.delete('/notes/:id', authenticate, authorize('admin', 'moderator'), deleteNote)
app.get('/notes', authenticate, authorize('user', 'moderator', 'admin'), getNotes)

Ownership checks (BOLA / IDOR)

Interview answer

A role check isn't enough. If Ali changes /notes/7 to /notes/8, his role check still passes, so I must also check that the note belongs to him: WHERE id = $1 AND owner_id = $2. Missing this is called BOLA or IDOR, the #1 API security bug.

Ali changes /notes/7 to /notes/8

Role check only

AliServerDatabase

Role + ownership

AliServerDatabase

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Role check only
Step through the diagram to see requests and responses.
Role + ownership
Step through the diagram to see requests and responses.
js
app.get('/notes/:noteId', authenticate, authorize('user', 'moderator', 'admin'), async (req, res) => {
  const canReadAny = ['moderator', 'admin'].includes(req.user.role)
  const { rows } = canReadAny
    ? await pool.query('SELECT * FROM notes WHERE id = $1', [req.params.noteId])
    : await pool.query('SELECT * FROM notes WHERE id = $1 AND owner_id = $2', [req.params.noteId, req.user.sub])
  if (!rows[0]) return res.status(404).json({ error: 'not found' })
  res.json(rows[0])
})

Generic errors and timing attacks

Interview answer

On login failure I always return the same message, "authentication failed", never "user not found" or "wrong password". I also make every failure take the same time by always running argon2.verify, with a real dummy hash when the user doesn't exist. Otherwise attackers can find out which emails are registered.

The timing attack

Email doesn't exist

AttackerServerDatabase

Email exists, wrong password

AttackerServerDatabase

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Email doesn't exist
Step through the diagram to see requests and responses.
Email exists, wrong password
Step through the diagram to see requests and responses.

Specific messages tell attackers what to try next:

  • user not found → this email doesn't exist
  • incorrect password → the email exists, now guess the password
  • account locked → the email exists

Always return 401 { "error": "authentication failed" }.

js
// FIX 1: always hash. The dummy must be a REAL argon2 hash, computed once at startup.
// (A fake string can make argon2.verify throw or return early, and the timing gap comes back.)
const dummyHashPromise = argon2.hash('dummy-password')

const hash = user ? user.password : await dummyHashPromise
const valid = await argon2.verify(hash, password)
if (!user || !valid) return res.status(401).json({ error: 'authentication failed' })

// FIX 2: constant-time comparison for secret strings (hash first so lengths always match)
const a = crypto.createHash('sha256').update(expected).digest()
const b = crypto.createHash('sha256').update(received).digest()
const match = crypto.timingSafeEqual(a, b)

Password reset

Interview answer

The user enters an email and I always reply with the same message, whether the account exists or not. If it exists, I email a random one-time link that expires in 15 minutes, and I store only a hash of that token. After the password changes, I delete the token and log the user out everywhere.

Password reset flow
AliServerRedis / DBEmailRedis / DB holds—

Press ▶ Play or “Next step”. Arrow keys work too.

Debug view

Step through the diagram to see requests and responses.
js
app.post('/forgot', async (req, res) => {
  const user = await findUserByEmail(req.body.email)
  if (user) {
    const token = crypto.randomBytes(32).toString('base64url')
    await redis.set(`reset:${sha256(token)}`, user.id, 'EX', 900) // 15 min, stored hashed
    queue.add('send-reset-email', { to: user.email, token }) // in the background: no timing difference
  }
  res.json({ message: 'If an account exists, we sent a reset link.' }) // always the same
})

app.post('/reset', async (req, res) => {
  const key = `reset:${sha256(req.body.token)}`
  const userId = await redis.get(key)
  if (!userId) return res.status(400).json({ error: 'invalid or expired link' })

  await redis.del(key) // single use
  await pool.query('UPDATE users SET password = $1 WHERE id = $2', [await argon2.hash(req.body.password), userId])
  await revokeAllSessions(userId) // log out everywhere
  res.json({ message: 'Password updated. Please log in.' })
})

Quiz

Think of the answer first, then reveal it.

0/15 answers revealed
  1. Q1 · concept

    What is the difference between authentication and authorization?

  2. Q2 · concept

    Why never store plain-text passwords, and why Argon2 instead of SHA-256?

  3. Q3 · concept

    What makes a JWT stateless, and is its payload encrypted?

  4. Q4 · concept

    How do you revoke a JWT immediately? What is the hybrid approach?

  5. Q5 · concept

    What is the difference between a session and a cookie? What does HttpOnly do?

  6. Q6 · concept

    What is the difference between OAuth 2.0 and OIDC? What does Google send back in the redirect?

  7. Q7 · concept

    What is refresh token rotation, and how is reuse detected?

  8. Q8 · concept

    What is RBAC? What happens if authenticate calls jwt.verify without try/catch?

  9. Q9 · concept

    What is a timing attack on login, and how do you prevent it?

  10. Q10 · scenario

    A user logs out but their JWT is still valid for 10 minutes. Is this a problem?

  11. Q11 · scenario

    Your app runs on five servers in three regions and uses microservices. Sessions or JWT? HS256 or RS256?

  12. Q12 · scenario

    Your login endpoint is being brute-forced, and a user reports someone else is using their account. What do you do?

  13. Q13 · concept

    What is CSRF, and how does a CSRF token stop it?

  14. Q14 · scenario

    A malicious app on the phone intercepts the authorization code in your mobile login. How does PKCE save you?

  15. Q15 · scenario

    Ali changes /notes/7 to /notes/8 and sees Sara's note, even though your RBAC check passed. What's the bug and the fix?