151: Authentication: Passwords, Sessions, Tokens, OAuth, and OIDC
Learning outcomes
By the end of this lesson, you can:
- explain and apply password storage in a realistic implementation;
- explain and apply server sessions in a realistic implementation;
- explain and apply bearer tokens in a realistic implementation;
- explain and apply refresh and rotation in a realistic implementation;
- explain and apply oauth 2.0 and oidc in a realistic implementation.
Prerequisites and retrieval
This lesson assumes the 01–06 foundation and the preceding lessons in this module. Before you begin, retrieve one concrete example from an earlier project where this concern appeared. It might have been a login form, a cookie, an API authorization header, or a third-party sign-in flow. The point is to connect the vocabulary to a system you have already seen, not to memorize a list of terms.
The goal is a defensible decision inside a production full-stack web application: a React client talking to a Node.js API, with persistent storage, authentication, observability, and deployment concerns. Authentication choices are not isolated library choices. They affect browser behavior, server state, incident response, testing, and the way a system behaves when credentials expire or are stolen.
Terminology
- Password storage: Never encrypt reusable passwords for later decryption. Store a salted password hash produced by a modern password hashing function, then compare a login attempt against that hash.
- Server sessions: A session identifier stored in an
HttpOnly,Securecookie can keep the credential out of JavaScript. The server maps that identifier to authenticated state and retains direct revocation control. - Bearer tokens: A bearer access token is a credential whose possession is enough to use it. Anyone who obtains it can present it until it expires or is otherwise rejected.
- Refresh and rotation: Long-lived refresh credentials need rotation, revocation, replay detection, and a bounded lifetime. Refreshing must not create an immortal credential by accident.
- OAuth 2.0 and OIDC: OAuth is delegated authorization: it lets a client obtain access to a resource. OpenID Connect, or OIDC, adds an identity layer on top of OAuth so the client can establish who authenticated.
- Logout and revocation: Logout semantics depend on the credential model. Clearing a browser cookie is different from revoking server state, and neither necessarily invalidates an already-issued bearer access token immediately.
Mental model
Treat Authentication: Passwords, Sessions, Tokens, OAuth, and OIDC as a design problem with observable inputs, outputs, invariants, and failure modes. Authentication proves who is acting; authorization decides what that actor may do. The mechanism must fit the browser threat model, revocation requirements, client type, and operational constraints.
A strong implementation makes its assumptions visible and narrows uncertainty at each boundary. Tests, types, database constraints, metrics, logs, and diagrams should provide evidence for why the design is safe. If the system cannot tell whether a credential was expired, revoked, replayed, or simply malformed, debugging and incident response will be unnecessarily difficult.
A useful interview and production sequence is:
requirement -> constraints -> model -> implementation -> failure analysis -> verification
Do not jump from a requirement directly to a library call. First state what must remain true. For example, a password must not be recoverable from the database, a session must be invalidatable, and an access token must be rejected outside its intended audience or lifetime. Then choose the mechanism that enforces those properties and decide how you will observe a violation.
Deep dive
1. Password storage
The first question is not which encryption key to put in an environment variable. Reusable passwords should not be decryptable by the application or by someone who obtains the password table. Store a salted password hash generated by a modern password hashing function instead. Salting prevents identical passwords from producing the same stored value and makes precomputed lookup attacks less useful; the password-hashing function should also make large-scale guessing expensive.
That storage rule is only one part of the login boundary. Apply rate limits and account abuse controls around login, and test malformed or missing credentials without revealing unnecessary information about which accounts exist. Do not log passwords or other reusable credentials while adding observability.
Decision rule: Use password storage deliberately when it makes the contract or invariant easier to prove. If a library or abstraction only reduces typing while hiding an assumption, prefer the more explicit design. The invariant to preserve is that a database compromise does not hand an attacker a collection of reusable plaintext passwords or decryptable password ciphertexts.
2. Server sessions
With a server session, the browser sends a session identifier and the server looks up the associated authenticated state. Putting that identifier in an HttpOnly, Secure cookie keeps the credential out of ordinary JavaScript access and limits exposure from some client-side attacks. Secure also ensures the cookie is sent only over an HTTPS connection. Cookie attributes do not replace the rest of the security design.
Because cookies are ambient credentials, the browser can attach them to requests without application code explicitly placing them there. State-changing requests therefore still need CSRF protection, such as an appropriate CSRF token strategy and origin checks where applicable. Rotate the session after a successful login so an identifier supplied before authentication cannot be fixed and reused as the authenticated session. On logout, invalidate the server-side session as well as clearing the browser cookie.
Decision rule: Use server sessions deliberately when they make the contract or invariant easier to prove. They are often a good fit when a server-rendered or browser-based application needs straightforward revocation. If an abstraction only reduces typing while hiding an assumption, prefer the more explicit design, and document where session state lives, how it expires, and how multiple application instances share it.
3. Bearer tokens
A bearer access token is a credential: possession is enough to use it. The API should validate its issuer, audience, signature, and expiry, and should reject tokens that do not satisfy the contract. Keep access-token lifetimes short enough to bound the impact of theft. Choose storage carefully because a JavaScript-readable token can be exfiltrated by an XSS attack.
Do not confuse a signed token with a harmless token. A valid signature says that the issuer created the token and that its contents were not changed; it does not make the token secret, guarantee that the user is trustworthy, or provide immediate revocation. Decide what the API can do when a user is disabled or a session is revoked while an access token is still within its lifetime.
Decision rule: Use bearer tokens deliberately when they make the contract or invariant easier to prove, such as when an API needs a credential that can be presented independently of a particular browser cookie. If they only reduce typing while hiding storage, audience, or revocation assumptions, prefer a more explicit design.
4. Refresh and rotation
Refresh credentials are usually longer-lived, so they deserve a stricter lifecycle than access tokens. Rotation means exchanging the current refresh credential for a new one and invalidating or superseding the old one. Store enough server-side state to revoke refresh credentials, detect reuse of an old credential, and associate the credential with the relevant account or client as required by the design.
Replay detection matters because a reused rotated credential can indicate theft. The response to that signal should be deliberate, which may include revoking the token family or requiring the user to authenticate again. Bound the lifetime of the refresh credential and consider concurrent refresh requests, retries, and atomicity so a legitimate retry is not mistaken for an attack or allowed to create inconsistent state.
A refresh endpoint should not simply mint unlimited new access tokens from an immortal token. It should validate the presented credential, enforce its expiry and revocation state, perform the rotation policy, and return a bounded result. Avoid putting refresh credentials in URLs or logs.
Decision rule: Use refresh and rotation deliberately when a short-lived access credential is needed without forcing a user to enter a password for every request. If the design cannot explain revocation, replay detection, lifetime, and concurrent refresh behavior, it is not ready to ship.
5. OAuth 2.0 and OIDC
OAuth is delegated authorization; it answers whether a client may obtain access to a resource. OAuth alone is not a general identity assertion. OpenID Connect adds identity semantics, including the information needed to establish who authenticated. This distinction is where people often get confused: receiving an OAuth access token does not by itself prove that its holder is a particular application user.
For modern browser and native flows, use Authorization Code with PKCE. Validate state to bind the callback to the initiated flow, validate nonce for OIDC replay protection, and validate redirect URIs against an exact registered allowlist rather than accepting arbitrary destinations. The callback, token, issuer, audience, and signature checks belong to the trust boundary; do not treat provider-supplied values as trusted merely because they arrived through the browser.
Decision rule: Use oauth 2.0 and oidc deliberately when delegated access or an external identity provider is part of the requirement. If the mechanism only hides the provider, redirect, token, or identity assumptions, prefer the more explicit design and state exactly which party is authorizing what.
6. Logout and revocation
Logout is not one universal operation. For a cookie-based session, clear the browser cookie and invalidate the server-side session. For refresh-based designs, revoke the refresh state or token family where supported. For already-issued bearer access tokens, be explicit about their remaining lifetime and about whether the resource server can check revocation before accepting them.
Also clear relevant client state without assuming that client-side navigation is security enforcement. A hidden button or a cleared React context cannot revoke a credential on the server. The credential model determines what logout can guarantee, so state that guarantee in the application contract and test it.
Decision rule: Use logout and revocation deliberately when they make the credential lifecycle understandable and enforceable. If the design promises immediate logout but only deletes local UI state while accepting an unexpired token, the promise and implementation do not match.
Worked example
Consider a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which concept above owns each failure mode. For example, the authentication boundary owns credential verification, the session or token layer owns credential lifetime, and the API authorization layer owns permission checks.
The useful separation is architectural as well as pedagogical: parsing and validation belong at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases and security reviews much harder to reason about.
export async function handleRequest(input: unknown) {
const command = parseCommand(input);
const result = await service.execute(command);
return toHttpResponse(result);
}
This snippet is intentionally small. input is untrusted, so parseCommand must establish the shape and constraints the service is allowed to rely on. The service should not be responsible for HTTP parsing, and toHttpResponse should not decide whether a password, session, or token is valid. Those boundaries make the ownership of failures visible.
Walk the example with at least four cases: the normal path; an empty or missing value; a duplicate, retry, or concurrent path where relevant; and a dependency failure. For each case, state which layer detects the problem and what the caller observes. A malformed request might fail at parsing, a duplicate account might fail through a persistence constraint, a revoked session at authentication, and a database outage as a dependency failure with an appropriate structured error. This is the level of explanation expected in a senior code review or technical interview.
Production perspective
Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. Authentication adds operational questions: where session or refresh state is stored, how clock differences affect expiry, how revocation is propagated, and which events are safe to record without logging secrets.
Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Metrics should help distinguish failed password verification, expired credentials, revoked sessions, invalid token claims, provider callback failures, and dependency outages. Optimize only after you can identify the bottleneck or risk with evidence.
When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations, especially for session rotation and replay detection. When it involves user-visible state, define loading, empty, error, stale, and success states. When it involves security, assume the client can be modified and all network input is untrusted. Do not expose tokens or credentials in URLs, logs, screenshots, or error messages.
Guided lab
Implement a cookie-based session login for a small app. Add secure cookie attributes, CSRF protection for writes, session rotation after login, logout invalidation, and tests for expired and revoked sessions. Keep the browser, API, and persistence responsibilities separate enough that a failing test tells you which boundary broke.
Complete the lab with this discipline:
- Write the requirement and two non-requirements.
- List input, output, and error contracts before implementation.
- Implement the smallest correct vertical slice.
- Add at least one invalid-input test and one edge-case test.
- Instrument or inspect the behavior instead of guessing.
- Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
- Explain one alternative design and why you did not choose it.
- Record a short “what would break at 10× scale?” note.
When inspecting the lab, verify more than a successful redirect. Check the cookie attributes in the browser, inspect the network request and response, confirm that a state-changing request without CSRF protection is rejected, and verify in storage that the pre-login session cannot be used after rotation. Test expiry and revocation as observable behavior rather than relying only on implementation details.
Edge cases and failure modes
- Password storage: test absence and malformed input, duplicate account attempts, ordering and concurrency where applicable, and behavior at the smallest and largest credible sizes. Confirm that failure responses do not leak reusable credentials or unnecessary account-existence information.
- Server sessions: test absence and malformed session identifiers, duplicates, ordering and concurrency where applicable, expiry, rotation, logout invalidation, and behavior at the smallest and largest credible sizes.
- Bearer tokens: test absence and malformed tokens, duplicates, ordering and concurrency where applicable, invalid issuer or audience, bad signatures, expiry, and behavior at the smallest and largest credible sizes.
- Refresh and rotation: test absence and malformed credentials, duplicates, ordering and concurrency where applicable, expired and revoked credentials, replay of a rotated credential, and behavior at the smallest and largest credible sizes.
- OAuth 2.0 and OIDC: test absence and malformed callbacks, duplicates, ordering and concurrency where applicable, state or nonce failures, unapproved redirect URIs, provider errors, and behavior at the smallest and largest credible sizes.
Common mistakes and debugging
- Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong for the client type, threat model, or revocation needs.
- Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary”
anyvalues. These can turn an invalid credential or malformed provider response into an apparently valid internal value. - Testing only the happy path and therefore discovering contracts only after integration. Expiry, replay, concurrency, and dependency failure are part of the credential lifecycle, not optional polish.
- Optimizing before measuring, or selecting a scalable mechanism without a scale requirement. More moving parts do not automatically produce a safer or faster authentication system.
- Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees. A disabled button is not an authorization check, and cleared UI state is not revocation.
For debugging, reproduce the smallest failing case and inspect the actual value or execution plan. Trace the boundary where the invariant first becomes false: source or build, browser or DOM, Network or HTTP, server or route, database or query, or deployment or configuration. Check the status code, cookie attributes, token claims, server logs, storage state, and relevant timestamps without exposing secrets. Then fix the owning layer rather than adding a downstream patch that merely hides the symptom.
Interview questions
- What problem does Password storage solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Server sessions solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Bearer tokens solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do Refresh and rotation solve, and what trade-off or failure mode would make you choose a different approach?
- What problem do OAuth 2.0 and OIDC solve, and what trade-off or failure mode would make you choose a different approach?
Checkpoint
Without notes, explain Authentication: Passwords, Sessions, Tokens, OAuth, and OIDC to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Be precise about the difference between proving identity, authorizing access, and managing the lifetime of the credential. Then implement a small example without copying the lesson code.
Mastery checklist
- I can define the core terms precisely.
- I can choose a design from requirements instead of from habit.
- I can implement and test the normal path and edge cases.
- I can explain the runtime, storage, or complexity cost.
- I can identify which layer owns validation, errors, and recovery.
- I can compare at least two reasonable alternatives.
- I can explain how the design changes at larger scale or stricter reliability.
