Signature Checks Catch Forged Tokens That Session Logs Never Record
Your access logs show a valid session. The user ID is correct, the timestamp is plausible, and every middleware check passed. But the token was forged. Signature verification would have caught it; session logs never will. This piece compares JWT signature checks with session-store auditing, explains what each genuinely gets right, and gives you concrete checks for your next auth review.
When Access Logs Lie by Omission
Session logs record who made a request and when. They do not record whether the token carrying that identity was signed by your server. A stolen cookie replayed through a load balancer looks identical to a legitimate request. The log line says 200 OK, and the session table confirms the ID exists. Nothing in that trail flags forgery.
Signature verification sits at a different layer. It asks whether the token was minted by a party holding the signing key. If the answer is no, the request dies before it reaches your business logic. That check catches tampering that log correlation cannot see, because the forged token never needed to steal a session ID. It simply asserted one.
Two tooling philosophies emerge. One trusts the session store as the source of truth and treats logs as the audit trail. The other trusts cryptography and treats logs as a secondary signal. In an incident timeline, the difference matters: the first approach shows a clean access record, the second shows a failed signature at the gateway.
There is a cost angle that rarely makes it into architecture documents. Log correlation means storing and indexing request records at volume. At even moderate traffic, retention windows of weeks or months turn into a storage bill that grows with every new service added to the fleet. Signature verification, by contrast, is a fixed compute cost per request that does not grow with retention policy. Teams weighing the two approaches often compare them on security alone and miss that one scales with traffic volume and the other with how long you keep history.
What JWT Signatures Actually Verify
JSON Web Signature (JWS) signs a set of claims. It does not encrypt them. Anyone holding the token can read the payload, including user IDs and roles. The signature proves the claims were not altered after signing; it says nothing about whether the claims are true or whether the token is still valid.
Two signing families dominate. HMAC uses a shared secret, so every service that verifies a token can also mint one. Asymmetric keys use a private key for signing and a public key for verification, which limits the blast radius if a verifier is compromised. The choice affects key rotation hygiene and how many parties you trust with signing capability.
Algorithm confusion attacks exploit weak library defaults. If a verifier accepts the algorithm named in the token header without checking it against an allowlist, an attacker can switch from an asymmetric algorithm to HMAC and sign with the public key. Libraries have shipped with permissive defaults, and upgrades alone do not fix the configuration. An explicit allowlist in every verification path does.
The none algorithm deserves separate mention because it is the simplest version of the same class of bug. Some libraries, when configured without a key, treat alg: none as a valid choice and skip the signature check entirely. This is not an exotic edge case; it is what happens when a service is stood up during a migration and the key material is never wired in. Verification that fails open is worse than no verification, because it produces confident-looking authorization decisions backed by nothing.
The Gap Session Stores Cannot Close
Server-side sessions centralize revocation. One delete from the session table logs a user out everywhere, and the audit trail is straightforward. That model works well for a single application with a shared data store. It also makes session fixation and replay easier to reason about because there is one record to inspect.
Horizontal scaling pushes teams toward stateless tokens. A session lookup on every request adds latency and a shared dependency. When services multiply, the session store becomes a coordination point that fails under load. Stateless tokens remove that hop, and the trade-off is that revocation becomes a distributed problem.
A forged token passes a session-ID lookup only if the signature was never checked. That is the gap. The session store can confirm an ID exists, but it cannot confirm the token presenting that ID was signed by your key. Logs show a valid-looking request; a signature check shows the forgery. Teams that rely on log correlation alone are auditing the wrong layer.
Replay is the other half of this gap. A session token captured from a network path or a browser extension can be re-presented for as long as it remains valid, and the session store has no way to distinguish the original holder from the copy. Binding tokens to a client fingerprint or a short expiry narrows the window, but the underlying problem is that the server cannot tell a copied credential from a genuine one. A signature check does not solve replay either, but it does confirm the token was minted by your key rather than assembled by hand, which removes an entire class of forgery from the threat model.
A Week Debugging a Phantom Login
An engineer traces unexplained admin access across three services. The session table shows nothing unusual: the admin user logged in at a plausible hour, and the token carried the right role claim. The services logged successful authorization. Nothing in the session records pointed to an intruder.
The JWT header told a different story. The token declared alg: none, and one downstream service accepted it because its verification library treated none as a valid algorithm when no key was configured. That service had been added during a migration and never received the signing key. It trusted every token that arrived.
The fix was a library upgrade plus a strict algorithm allowlist on every service. The postmortem added signature verification to the gateway layer so that no request reaches a downstream service without a verified signature. The session logs from that week remain clean, which is the point: they always will.
What made this case expensive was the detection delay. The forged access happened days before anyone noticed, and the investigation consumed most of a week because the team started from the assumption that the session table was authoritative. Once they shifted to inspecting token headers, the answer took minutes. The lesson is about where you look first, not about how clever the attack was.
Trade-Offs That Decide Architecture
Stateless tokens ease scaling and complicate immediate revocation. A stolen token remains valid until it expires unless you maintain a revocation list, which reintroduces the shared state you removed. Short lifetimes reduce the window but increase refresh traffic and the chance of race conditions in client code.
Centralized sessions ease revocation and add latency plus storage. Every request touches the session store, and that store becomes a high-availability concern. For a small number of services, the cost is manageable. For a large fleet, it becomes a bottleneck that teams work around, often by caching session data in ways that weaken the revocation guarantee.
Hybrid designs pair short-lived access tokens with a revocation list checked at the gateway. The list can be small and replicated, and the tokens expire quickly enough that list staleness is bounded. The objection from platform teams is operational: you now run two systems and must keep their clocks and failure modes aligned. A related piece on retry budgets and microservice failures makes a similar point about coordination costs.
Threat model, not fashion, should pick the default. If immediate revocation is a compliance requirement, centralized sessions or a revocation list are not optional. If the fleet is large and tokens are short-lived, stateless verification with strict algorithm checks may be the better fit. Neither choice removes the need for signature verification.
Practical Checks for Your Next Auth Review
Enforce an algorithm allowlist in every verification path. Check the token header against a fixed set of permitted algorithms before any signature check runs, and fail closed when the header names something else. Do this in the gateway and in each service that verifies tokens independently.
Log signature failures separately from authentication failures. A failed signature is a different signal from a wrong password, and mixing them hides forgery attempts in a sea of expired sessions. Route signature failures to a distinct metric so that a spike is visible without querying raw logs.
Rotate signing keys on a schedule you can audit. Publish key IDs in token headers, keep old public keys available for verification during an overlap window, and record each rotation in a change log. A related piece on certificate pinning and rotation covers the failure mode when rotation lags.
Test forged-token scenarios in staging, not production. Craft tokens with alg: none, with a swapped algorithm, and with a modified role claim, then confirm each is rejected. Run the same test against every service that verifies tokens, including the ones added during migrations.
Review the services that never received the signing key. Inventory every verification path and confirm it has the key material and the allowlist configured. The phantom login in the previous section came from exactly that gap, and session logs gave no warning.