Track 2 / Guide 07
JWT Security Architecture
Design JWT verification, key rotation, replay controls, and browser storage without confusing decoded claims with trusted authorization.
On this page
Model a JWT as a credential with a lifecycle
A JSON Web Token is a container used within an authentication and authorization system. Its safety depends on how it is issued, transported, verified, expired, and revoked. A token that parses successfully is not trusted, and a token with a valid signature is not automatically appropriate for every endpoint. The verifier must establish the intended issuer, audience, token type, time window, and permitted use.
Start with a concrete threat model: which service issues tokens, which services consume them, whether clients are browsers or machines, and what an attacker could do with a stolen credential. Decide whether offline verification is actually needed. A conventional opaque session identifier backed by server state can be simpler when immediate revocation and browser sessions are the primary requirements.
Do not use a token payload as a place to store secrets merely because its representation is compact. A signed token is commonly readable by anyone holding it. Minimize claims, avoid personal data that every intermediary does not need, and set a clear retention policy for logs. The JSON security guide explains why parsing and validation are distinct from trust; that distinction is especially important when the parsed object is a credential.
Distinguish JWS, JWE, and untrusted decoding
A compact signed token typically has header, payload, and signature segments encoded with Base64url. Decoding the first two segments is useful for inspection but performs no cryptographic verification. Treat their contents as attacker-controlled until the verification operation succeeds. Do not select an unrestricted algorithm or a remote key location simply because the header requests it.
JWS provides integrity and authenticity under the configured key model, while JWE provides encryption with its own cryptographic structure and processing rules. Signing does not hide claims, and encryption does not eliminate the need to validate who issued a token and where it may be used. Nested constructions add complexity and should follow a reviewed protocol rather than a custom sequence of string operations.
Keep token types distinct. An identity token meant for a client is not necessarily an access token for an API. A password-reset token should not be accepted by a general session verifier. Use explicit verifier configurations and mutually exclusive validation rules where token purposes differ. Sharing one permissive decode-and-check helper across every credential type often creates cross-context acceptance that signature verification alone cannot detect.
Pin verification policy instead of trusting headers
The server should define acceptable algorithms and key sources. Historical algorithm-confusion failures arose when applications let token-controlled metadata change how a key was interpreted or accepted an unsigned algorithm unexpectedly. A modern library helps, but only when its verification interface is configured deliberately and the application does not bypass errors.
Validate issuer and audience exactly according to the deployment contract. Require expiration when the system relies on bounded lifetime, and apply a small documented clock tolerance rather than a large arbitrary allowance. Check required claims explicitly. An optional claim that is verified only when present may not satisfy a policy requiring every token to contain it.
Reject unknown key identifiers through a bounded resolution path. A key identifier is a lookup hint, not a filename or a free-form network destination. Avoid constructing filesystem paths or URLs directly from it. If remote key sets are used, trust a configured issuer endpoint, apply caching and timeouts, and prevent unknown identifiers from causing unlimited network refreshes. These controls protect both authenticity and service availability during malformed-token floods.
Runnable signing and verification with an allowlist
The example generates an ephemeral key pair, signs a short-lived token, and verifies it using an explicit algorithm, issuer, audience, and required-claim policy. Save it as jwt.mjs, install jose 6.x in a local project, and run it with a supported Node.js runtime. Ephemeral keys make the example self-contained; production signing keys need protected storage and an operational rotation plan.
The application reads the subject only from the verified result. It does not decode claims first and use them to authorize a request while verification happens elsewhere. The type check further distinguishes this access-token profile from other tokens that might share infrastructure. Business permissions still need application logic after verification; a subject identifies a principal, not unrestricted access to every object.
Do not paste production credentials into debugging tools or publish them in test fixtures. Use synthetic tokens generated for the test environment. The example's issuer and audience are deliberately local contract values and must match your real trust configuration when adapted. Handle verification exceptions as authentication failures with a bounded error response, without returning cryptographic internals or the original credential to the caller.
import { generateKeyPair, SignJWT, jwtVerify } from "jose";
const { publicKey, privateKey } = await generateKeyPair("RS256");
const issuer = "https://issuer.example";
const audience = "orders-api";
const token = await new SignJWT({ scope: "orders:read" })
.setProtectedHeader({ alg: "RS256", typ: "at+jwt", kid: "demo-key" })
.setIssuer(issuer)
.setAudience(audience)
.setSubject("user-demo")
.setIssuedAt()
.setExpirationTime("5m")
.sign(privateKey);
const { payload, protectedHeader } = await jwtVerify(token, publicKey, {
algorithms: ["RS256"],
issuer,
audience,
requiredClaims: ["sub", "iat", "exp"],
clockTolerance: 5,
});
if (protectedHeader.typ !== "at+jwt") {
throw new Error("Unexpected token type");
}
console.log({ subject: payload.sub, scope: payload.scope });Replay requires state or binding, not better JSON
A stolen bearer token can generally be replayed while it remains valid. A signature prevents modification but does not prove that the current presenter is the original recipient. Short lifetimes reduce the window but do not eliminate it. Decide whether the risk requires sender-constrained credentials, a server-side session check, or a targeted revocation mechanism.
A unique token identifier is useful only if the application gives it semantics. Merely adding one does not prevent replay. For a one-time operation, atomically record consumption and reject subsequent attempts within the relevant retention window. For ordinary access tokens, checking every identifier can reintroduce a stateful dependency, which may be entirely reasonable but should be acknowledged in the architecture.
Separate authentication replay from business-operation replay. A valid user can accidentally retry a purchase after a timeout. That requires an idempotency or transaction strategy even when the token is secure. The API response guide discusses retry semantics, while the patch guide explains conditional updates. Credential validity cannot replace concurrency control or duplicate-operation detection.
Rotate keys without creating an outage window
Publish a new verification key before issuing tokens signed with it, then retain the previous key long enough to verify still-valid tokens under the planned lifetime and clock tolerance. Monitor adoption and verification failures during the overlap. Removing an old key too early creates an outage for legitimate clients; keeping compromised keys indefinitely defeats the purpose of rotation.
Distinguish routine rotation from emergency revocation. Routine overlap is appropriate when the old key remains trusted. A suspected compromise may require invalidating tokens and forcing reauthentication, even at a usability cost. Document that decision path before an incident, including which services cache keys and how those caches can be refreshed safely.
Bound remote key-set refresh frequency and cache size. Attackers can send many unknown identifiers, so a cache miss must not cause unlimited outbound requests or unbounded memory growth. Observe refresh failures separately from invalid signatures. A network problem fetching keys should not silently switch the verifier into accepting unverified tokens. Fail according to the service's explicit availability and trust policy, with clear metrics for operators.
Choose browser storage with the whole threat model
HttpOnly cookies prevent JavaScript from reading a cookie value directly, while Secure restricts its transmission to secure contexts. SameSite settings influence cross-site request behavior. These attributes are useful controls, but they do not make cross-site scripting harmless: injected script may still perform authenticated actions through the victim's browser.
Cookie-based authentication also requires a cross-site request forgery strategy appropriate to the application. Combine suitable SameSite behavior with origin checks or anti-CSRF tokens where needed, especially for state-changing operations. Do not describe a cookie attribute as a universal replacement for request-level protections. Keep cookie scope narrow and avoid exposing credentials to unrelated subdomains.
Browser storage accessible to JavaScript has a different risk profile because an injected script can read and exfiltrate tokens. In-memory storage reduces persistence but changes refresh and navigation behavior. There is no storage location that removes all attack classes. Choose the session architecture first, then align storage, refresh, logout, and revocation behavior with it. Avoid placing tokens in URLs, where they can leak through history, copied links, and infrastructure logs.
Test the verifier as a security boundary
Build negative fixtures for wrong audience, wrong issuer, expired tokens, missing required claims, unexpected token type, unsupported algorithm, and unknown key identifiers. Verify that every rejection occurs before protected business logic runs. Include a valid token for a different purpose to catch cross-context acceptance. These tests should exercise the actual middleware configuration rather than a separate test-only verifier.
Test rotation with old and new keys and simulate key-fetch failures. Measure how the service behaves under repeated invalid tokens without logging credential contents. Authentication failure metrics should use bounded categories, not arbitrary attacker-controlled header values. Review every catch block to ensure a verification exception cannot become a successful anonymous or privileged fallback.
Finally, audit logout and account-disable behavior against the promised user experience. If access tokens remain valid until expiration, say so in the architecture and ensure sensitive operations can consult current account state when required. A clear, limited guarantee is safer than claiming immediate revocation while relying only on offline signature checks. Maintain the verifier configuration as carefully as the signing service because both define the system's trust boundary.
Refresh tokens and account changes require separate state
Access-token verification and refresh-token management solve different lifecycle problems. A short-lived access token limits exposure, while a refresh credential allows a client to obtain another one without repeating the full login flow. Protect the refresh path as a high-value operation and define how it reacts to account disablement, password changes, and suspected compromise.
Refresh-token rotation can make reuse detectable when the server tracks the token family and replaces credentials atomically. A race between two legitimate requests needs a deliberate policy so ordinary client concurrency does not look indistinguishable from theft. Do not describe rotation as stateless if the security property depends on recording prior use.
Keep access and refresh token audiences, types, lifetimes, and storage rules distinct. An API should never accept a refresh credential as an ordinary bearer access token. Likewise, an access token should not authorize refresh simply because it has a valid signature. Separate verifiers and explicit token profiles reduce this cross-context risk.
Account authorization can change before an access token expires. For highly sensitive operations, consult current server-side state when the business requirement demands immediate effect. For lower-risk operations, a short documented validity window may be acceptable. The important point is that product promises about logout or account suspension must match the actual verification path.
Plan incident response before a signing key is lost
Maintain an inventory of token issuers, key stores, verification caches, and maximum credential lifetimes. During an incident, responders need to know which services will continue accepting an old key and for how long. A key identifier alone does not reveal every cache or offline verifier that depends on it.
Exercise an emergency rotation in a nonproduction environment. Remove the compromised key according to the scenario, issue a new trusted key, and verify that services fail closed while legitimate clients receive the intended reauthentication behavior. Measure propagation time and identify services that require a restart. This drill often exposes assumptions hidden by routine overlapping rotation.
Keep signing privileges narrow. A service that only verifies tokens should not possess a signing private key. Separate environments so a development credential cannot mint a production identity. Audit administrative access to key material and avoid exporting private keys merely to simplify local debugging.
During investigation, collect token metadata only under a controlled policy. Full tokens are reusable credentials until expired or revoked and should not be copied into ordinary tickets or chat logs. A fingerprint, issuer, key identifier, and verified timing metadata can support correlation without distributing the credential itself.
Finally, test recovery with real client flows: login, refresh, logout, expired sessions, and disabled accounts. Cryptographic correctness is necessary, but the incident outcome also depends on client retry behavior, clock configuration, and support communication. Document those dependencies so an emergency change does not accidentally create an endless refresh loop or an unbounded authentication load spike.
Engineering Comparison
| Control | Primary purpose | Remaining risk | Operational requirement |
|---|---|---|---|
| Signature verification | Detect tampering | Stolen token replay | Trusted keys and algorithm policy |
| Audience and issuer | Prevent cross-context use | Authorized misuse | Exact deployment configuration |
| Short expiration | Limit credential lifetime | Replay before expiry | Refresh and clock policy |
| HttpOnly cookie | Reduce direct script theft | XSS actions and CSRF | Cookie and request controls |
| Token consumption state | Enforce one-time use | State availability | Atomic storage |
| Key rotation | Replace signing material | Old valid tokens | Managed overlap or revocation |
Use a maintained JOSE implementation. The example demonstrates verification configuration, not a complete identity provider or session-management system.