How JWT Authentication Actually Works

· Medium ·

5 min read Original article ↗

Hideaki Takahashi

A tutorial guide to what a token contains, how its signature is checked, and where the trust boundary lies.

When you log in to an application, how does it recognize you on the next request? One common answer is a JSON Web Token, or JWT. To put it simply, it’s a token issued by a server, and the client presents it later. The service that receives it from the client checks whether it should trust that token or not.

What is JWT?

In a conventional server-side session, an application stores session state and gives the browser a session identifier, often in a cookie. On a later request, the application looks up the identifier.

JWT-based design instead allows the client to present a singed token, often as Authorization: Bearer <token> or through a cookie. Then, the service verifies the token and checks its claims before deciding what the request may do.

One of the advantages of tihs JWT-based authentication is that asigned JWT can reduce the need for a central session lookup on every request. Ofcourse, it does not mean the application has no other state. Account status, permissions, revocation, and business rules may still require a database or another service.

Also, JWT is a format, not a login protocol. A JWT may serve as an ID token or an access token, depending on the system. Those tokens have different intended recipients and must not be treated as interchangeable.

What is inside a signed JWT?

The common compact form has three Base64url-encoded segments separated by dots:

base64url(header).base64url(payload).base64url(signature)

The header describes how the token is protected. For example:

{"alg":"HS256","typ":"JWT"}

The payload contains claims (the bellow is a simplified example with illustrative values):

{
"iss": "https://login.example.test",
"sub": "user-123",
"aud": "https://api.example.test",
"exp": 1893456000
}

iss identifies the issuer; sub identifies the subject; aud names the intended recipient; and exp is an expiration time expressed as a Unix timestamp. Systems may add claims such as scopes or roles, but a claim is only useful if the receiving service interprets and enforces it correctly.

The signature protects the encoded header and payload against undetected changes. For an HS256 token, the issuer computes an HMAC over the first two segments using a shared secret, then Base64url-encodes the result.

Note that Base64url is encoding, and not encryption. Anyone holding an ordinary signed JWT can decode its header and payload.

Verification process?

Suppose someone changes sub from user-123 to admin-1. The first two encoded segments change, but the old signature remains tied to the original bytes. Thus, averifier with the correct key detects the mismatch.

The receiving service should also check that the token is unexpired, comes from the expected issuer, is meant for this service, and is of the expected token type. It should enforce the permissions needed for the specific operation. The application must choose which signing algorithms and keys it accepts; it must not let an attacker-controlled header expand that policy.

In short, the signature says that the protected bytes were issued by a party with the signing key. It does not by itself say that this token is suitable for this API request.

Flow 1: the application issues and checks its own token

Imagine an application that uses HS256, and it keeps a strong shared secret on the server.

  1. The user submits login credentials to the application.
  2. The application authenticates the user and creates a short-lived JWT with appropriate claims.
  3. It computes the HMAC with its secret and returns the token to the client.
  4. On a later request, the client presents the token.
  5. The application verifies the HMAC and checks the claims and the requested operation before responding.

The client never needs the signing secret. Every service that verifies an HS256 token, however, also has the secret needed to create one. Note that it is important to securely design and implement secret distribution when multiple services are involved.

Flow 2: an identity provider issues the token

Now separate the issuer from the application. An identity provider signs a token with its private key; the application verifies it with the corresponding public key. The application can check the token without receiving the issuer’s private key.

  1. The user authenticates with the identity provider through the appropriate login flow.
  2. The provider issues a signed token intended for a particular client or resource.
  3. The client presents the appropriate token to the intended recipient.
  4. The recipient obtains the provider’s trusted public keys, commonly through a published key set that it can cache.
  5. The recipient verifies the signature and the relevant claims before granting access.

The distinction in step 3 matters, where in OpenID Connect, an ID token tells the client application about the user’s authentication and is addressed to that client. An access token is what a client typically presents to a resource API. Being signed by a familiar provider does not make an arbitrary ID token a valid access token for your API. Likewise, social login can establish identity, but the application may still need to create a local account or session and apply its own authorization rules.

A small security checklist

  • Verify the signature using a configured algorithm and trusted key.
  • Check iss, aud, exp, and any other claims required by your protocol.
  • Keep tokens short-lived where practical and plan how logout or revocation works.
  • Treat roles and scopes as authorization inputs, then check the requested resource and action.
  • Do not put secrets in a readable payload or expose bearer tokens in logs and shared screenshots.

Exploring JWT-protected requests with h5i

h5i is a red-teaming workspace that lets an agent browse the app and inspect the HTTP requests it produces. You can easily capture an authenticated request, inspect its Authorization header or cookie, replay it with a controlled change, and compare the response. For example, you can check whether an expired JWT token is rejected or whether an endpoint enforces the permissions represented by a valid token.