JWT Authentication, End to End: How a Token Gets Signed, Stored, Carried, Verified, and Guarded
Most developers learn JWT by memorizing `sign` and `verify` without understanding the full request lifecycle. Seeing the five-stage chain—and why the payload is encoded rather than encrypted, why the token lives in two frontend stores, and how the interceptor and guard fit together—turns scattered API knowledge into a coherent mental model that makes debugging authentication failures straightforward.
JWT authentication is often reduced to calling `sign` and `verify`, but the real understanding comes from tracing a token's entire journey. The process breaks into five linked stages: the backend signs a payload with a secret to produce a tamper-proof token; the frontend stores it in both localStorage for persistence and a Zustand store for reactivity; an Axios request interceptor attaches it to every outgoing request; the backend verifies the signature without any database lookup; and a React route guard component redirects unauthenticated users before protected pages render.
A key clarification is that the JWT payload is Base64-encoded, not encrypted—anyone can decode it. Security relies entirely on the signature, which is an HMAC of the header and payload using a server-side secret. This makes the token self-verifying and stateless, which is why JWT replaced server-side sessions in distributed systems: any machine with the secret can validate the token, eliminating the need for shared session storage.
The frontend storage pattern addresses two separate concerns. localStorage survives page refreshes, while the Zustand store triggers UI re-renders when authentication state changes. The interceptor pattern centralizes a cross-cutting concern, and the route guard component follows React's declarative philosophy by expressing access control at the routing layer rather than inside individual pages.
Storing the token in two places—localStorage and Zustand—is not redundancy but a clean separation of persistence from reactivity, a distinction that applies to nearly all frontend state.
The interceptor pattern is an example of aspect-oriented programming in the frontend: extracting a cross-cutting concern (auth headers) from business logic so it's configured once and applied everywhere.
Understanding that the response interceptor unwraps `AxiosResponse` explains why some codebases access `res.data` and others don't—it depends on whether that unwrapping convention is in place.
The declarative route guard (`<RequireAuth><Pay /></RequireAuth>`) is more maintainable than imperative checks inside components because it makes access policy visible at the route definition level.