Authentication in a Backend
Turning an untrusted credential into an authenticated principal, once, early, in one place — and nothing more than that.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind has a problem.
What is the backend actually doing when it authenticates a request?
Every request that touches user data has to know which user it is acting for. The endpoints should not each solve that separately.
Each handler reads the token or cookie it expects, looks up the user, and carries on. It is explicit, it is local, and you can see exactly what every endpoint does.
A new endpoint is added and the check is forgotten. Nothing fails — the endpoint works, for everybody, including callers with no credential at all.
- A new endpoint is added and the check is forgotten. Nothing fails — the endpoint works, for everybody, including callers with no credential at all.
- Three handlers verify the credential three slightly different ways, and one of them treats an expired token as valid because it checked the signature and not the expiry.
- The credential type changes — a cookie session is joined by a bearer token for the mobile app — and every handler must be edited.
- A handler resolves the user but never records it, so the request log, the audit trail and the error report all say
nullfor the one field that would have answered the question. - Authentication runs after expensive work, so unauthenticated traffic still costs you a database query per request (Authenticate First, or Rate-Limit First?).
What is actually happening
- Authentication is a function from an untrusted credential to a principal, or to a refusal. Everything else in this module is a different way of carrying the credential and a different way of verifying it.
- A credential is a bearer of a claim, and the claim is always "I am this subject". Verification either checks a stored secret (a password, a hashed API key), looks up server-held state (a session id), or checks a signature (a token). Those three verification styles have different costs and different revocation stories.
- The output is a principal attached to the request context, not a boolean. Downstream code needs the subject id, the tenant, the credential type and the scopes it was granted — a boolean cannot answer "which user" (Request Context Propagation).
- It runs once, early, in the middleware pipeline, so no route can be added without it and no handler can disagree about it (The Middleware Pipeline).
- It says nothing about permission. "This is user 42" and "user 42 may read order 900" are separate questions, answered by separate code, and conflating them is the single most consequential mistake in this area (Authentication vs Authorization).
- Anonymous is a valid outcome. Public endpoints have an unauthenticated principal, not a missing one — modelling it explicitly stops "no user" and "we did not check" from looking identical.
Credential in, principal out
The useful mental model is a single narrow function. Bytes arrive claiming an identity; something verifies them; a principal or a refusal comes out. Every scheme in this module — password, session, token, OAuth, API key — differs only in how the credential is carried and how the verification is done.
Drawing the boundary this sharply is what keeps authorization out. The function has no idea what the caller is trying to do, and that is not a limitation; it is the reason the two concerns can be tested and changed independently.
The order of the chain is a cost decision and a correctness one
Authentication is not a single step; it is a position in a pipeline where every earlier step is cheaper and every later step depends on it. Putting it too late means paying for work on requests you were going to refuse. Putting the rate limiter after it means an attacker chooses how much verification CPU you spend.
The failure column is the part worth memorising: each step has a way of quietly not running.
- 1Edge limits
Body size, connection and request-rate caps before any application code.
fails by Absent, so unauthenticated traffic reaches the runtime at full rate.
- 2Correlation id
Assigns or accepts a request id so everything after is attributable.
fails by Assigned after auth, so auth failures are unattributable.
- 3Rate limit
Bounds attempts per IP, per key, per subject.
fails by Placed after verification, so the expensive step is the unprotected one (Authenticate First, or Rate-Limit First?).
- 4Extract credential
Reads cookie,
Authorizationheader or key header; nothing else.fails by Accepting several locations and picking the wrong one when more than one is present.
- 5Verify
Session lookup, signature check, or hashed-key comparison.
fails by Fail-open on store error; expiry not checked; non-constant-time comparison.
- 6Attach principal
Puts a typed principal on the request context.
fails by Stored somewhere not request-scoped, so it leaks across concurrent requests.
- 7Authorize
Decides whether this principal may do this thing to this object.
fails by Skipped, because authentication succeeded and that felt like enough (Object-Level Authorization).
Choosing how the credential is carried
This is the decision the rest of the module elaborates. There is no ranking: the criteria are who the client is, whether you need immediate revocation, and whether every request can afford a lookup.
Most real systems end up supporting two or three of these at once, which is fine as long as they all produce the same principal type and all go through the same verification step.
Who is calling, from where, and what does revocation have to look like?
when A browser-based first-party app; you want server-held state and instant revocation (Session Authentication).
cost A store lookup per request, cookie attributes to get right, and CSRF becomes your problem (CSRF Defense in Security Engineering).
when Mobile clients, service-to-service calls, or verification without a shared store (Token Authentication and the Revocation Problem).
cost Revocation is not immediate; key rotation and clock skew become operational concerns.
when You want the bearer-header ergonomics of a token with the revocation of a session.
cost A lookup per request — the same dependency a session has, without the cookie semantics.
when Machine callers with long-lived, coarse identity (API Keys).
cost Long-lived secrets leak; rotation and per-key scoping are yours to build.
when A third party must act for a user, or you are consuming an external identity provider (OAuth and OIDC From the Backend Side).
cost Protocol complexity, an external dependency in your login path, and a library you must keep current.
when Service-to-service inside a network you control, where identity should be a property of the connection.
cost Certificate issuance, rotation and revocation infrastructure; awkward for anything with a browser in it.
How to build it
Most important first.
- Authenticate in middleware that runs for every route, and make the unauthenticated path an explicit allowlist rather than an opt-in check per handler.
- Produce a typed principal — subject, tenant, credential type, scopes, issued-at — and put it on the request context. Handlers read it; they never re-derive it (Request Context Propagation).
- Support more than one credential type behind one interface. A cookie session for the browser, a bearer token for the mobile client and an API key for machines can all produce the same principal type (Session Authentication, Token Authentication and the Revocation Problem, API Keys).
- Order the pipeline deliberately: cheap rejections and rate limiting before expensive verification, so an unauthenticated flood does not cost you database work (Authenticate First, or Rate-Limit First?, Middleware Ordering Is a Correctness Decision).
- Fail closed. A verification error, a malformed credential or an unreachable session store is a refusal, not a fall-through to anonymous.
- Emit the principal id on every log line and span, so a request can be attributed after the fact (Correlation Ids That Survive Every Hop, Structured Logging).
- Return a uniform failure.
401with no detail about *why* — expired, unknown subject, bad signature — because the difference is useful mostly to an attacker (Not Leaking Your Internals).
What can go wrong
- Fail-open on store errors: Redis is unreachable, the lookup throws, the catch block logs and continues, and every request is anonymous — or worse, treated as valid (Fail Open vs Fail Closed in Security Engineering).
- A route registered outside the middleware chain — a health check, a debug route, a static handler, a second router mounted later — that skips authentication entirely (Health Checks: Startup, Readiness, Liveness).
- Authentication succeeding on a credential that has been revoked, because revocation is checked in a place the fast path skips.
- The principal is attached to something request-scoped in name only — a module-level variable, a global "current user" — and under concurrency one request sees another's identity (Backend Races).
- Clock skew between the issuer and the verifier, so freshly issued credentials are rejected or expired ones accepted.
- The gateway authenticates and the service trusts a plain header, so anyone who can reach the service directly can set that header (The Trust Boundary).
- A request that begins just before a credential is revoked can complete after it — verification and revocation are not one atomic step, and there is always a window whose width is your caching and propagation delay.
- Concurrent requests sharing one principal object that is mutated during the request (a lazily loaded profile, a cached permission set) can observe each other's partial state (Backend Races).
- Every credential arrives from outside the process and is attacker-controlled until verified. Length, encoding and structure are all input (Every Input Surface).
- Compare secrets in constant time. A comparison that returns early on the first differing byte is a timing oracle for anything an attacker can submit repeatedly.
- Rate-limit the authentication endpoints specifically. They are the ones an attacker replays, and they are usually the most expensive per request (Rate Limiting).
- Never log the credential. Tokens, session ids and API keys in a request log are a credential store with no access control (Secrets in Logs).
- If a gateway authenticates upstream, the service must either verify the credential itself or verify that the header came from the gateway — usually by mutual TLS or a signed internal token. "It is internal" is not verification (Defence in Depth).
- Authentication does not imply authorization, and a system that treats "logged in" as "allowed" has one permission level: everyone (Where the Check Belongs).
- "The user is authenticated, so this request is allowed." Authentication answers who, never what (Authentication vs Authorization).
- "The gateway does auth, so the service does not need to." The gateway usually does authentication only, and only for traffic that goes through it.
- "HTTPS means the credential is safe." TLS protects it in transit. It does nothing about where it is stored, how long it lives, or who can replay it (TLS as a Security Boundary in Security Engineering).
- "We use JWTs, so we are stateless." You are stateless about sessions and stateful about revocation, key rotation and clock — the state moved rather than disappeared (Stateless Services).
Operating it
- Count authentication outcomes by reason: success, expired, invalid signature, unknown subject, revoked. The shape of that distribution is how you notice credential stuffing before your users do.
- Emit the principal id and the credential type as fields on every request log line (Structured Logging).
- A latency histogram for the verification step alone — this is where a session-store round trip or an unexpectedly slow key fetch shows up.
- Alert on the ratio of 401s to total requests per client, not just on the absolute count.
- Count requests handled with an anonymous principal on routes that should never be anonymous. A non-zero value means a route escaped the chain.
- Verification cost is per request and it never amortises. Whether that is a signature check on CPU or a network round trip to a session store is the decision the rest of this module is about (Where Sessions Live, Token Authentication and the Revocation Problem).
- At 10x, an authentication middleware that makes a database query per request is the first thing to exhaust the connection pool (Connection Pools).
- At 100x, the identity store becomes a shared dependency of every service, and its availability becomes your availability — which is the argument that pushes teams toward signed tokens, and toward the revocation problem that comes with them.
- One central middleware makes the check unskippable and makes it a single point of failure and a single place to get subtly wrong.
- A rich principal object is convenient and tempts handlers into deriving permissions from it, which quietly moves authorization into the wrong layer.
- Uniform, detail-free 401s are correct and make legitimate client debugging harder — the detail belongs in your logs, keyed by correlation id.
Where this applies
Backend advice is context-sensitive. These labels say what each claim is specific to, and where a different stack or scale would differ.
- GENERALThe credential-to-principal model and the once-early-in-one-place rule hold across stacks and protocols.
- FRAMEWORK-SPECIFICWhere the check can be bypassed differs: Express middleware applies only to routers mounted after it, so a later
app.useescapes it; Django and Rails apply a global middleware stack but let individual views opt out with a decorator orskip_before_action; Spring Security matches by URL pattern, so an unmatched path is unprotected by default. Each has a different way to be accidentally unprotected. - PROTOCOL-SPECIFICOver HTTP the credential rides in a header or cookie on every request. Over a WebSocket it is presented once at the upgrade and the connection then outlives it, so revocation has to close the connection; over a message queue there is no caller at all and identity has to be in the message (WebSocket Message Contracts in API Design).
Where the depth lives
This domain teaches the application-side mechanism and hands the rest off.