Skip to main content
ANVISoftware Solutions
Lesson 3 of 14Intermediate18 min

Tokens and JWT

By the end of this lesson

Issue and validate tokens correctly, including signature and expiry checks.

A token is a credential the client presents on every request. The server checks it and, if it holds up, treats the request as coming from the account named inside it. No session lookup in a shared store is needed, which is why tokens suit APIs, mobile clients and systems split across several services.

A JSON Web Token, almost always shortened to JWT, is one common format for that credential. It is a single string in three parts separated by dots: a header, a payload, and a signature. The header and payload are JSON, base64url-encoded. The signature covers the first two parts, so any change to either one invalidates it.

It is called a bearer token because holding it is sufficient. There is nothing else in the request proving the holder is the person it was issued to. That single property is why every question about who can obtain a copy of a token deserves a careful answer.

The payload of an access token, base64url-decoded
JSON
{
  "iss": "https://identity.example-company.com",
  "aud": "orders-api",
  "sub": "e-4821",
  "employee_id": "4821",
  "role": "OrdersReader",
  "iat": 1735689600,
  "exp": 1735690500
}
  • This is the middle section of a token. Before it sits a small header naming the signing algorithm and the key that was used; after it sits the signature.
  • iss is the issuer: which identity service minted this. aud is the audience: which API it was minted for. sub is the subject: the account it refers to. All three are standard claim names, so libraries understand them without configuration.
  • iat and exp are Unix timestamps, meaning seconds since 1 January 1970. This token was issued and expires fifteen minutes later.
  • employee_id and role are application claims. Reading permissions from here rather than querying the employee table on every request is the efficiency the format buys — and the reason a role change does not take effect until a new token is issued.
  • Nothing in this payload is confidential, and that is not an accident of the example. See the warning below.
Program.cs — what validating a token actually checks
C#
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // Where to fetch the issuer's public signing keys from, over HTTPS.
        options.Authority = "https://identity.example-company.com";
        options.Audience = "orders-api";

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,   // signed with a key we trust
            ValidateIssuer = true,             // minted by our identity service
            ValidIssuer = "https://identity.example-company.com",
            ValidateAudience = true,           // minted for THIS api
            ValidAudience = "orders-api",
            ValidateLifetime = true,           // not expired
            ClockSkew = TimeSpan.FromSeconds(30),
            ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
        };
    });
  • Each flag is a separate question, and all of them matter. A token can carry a perfect signature and still be the wrong token for this API.
  • ValidateIssuerSigningKey confirms the signature was produced with a key this API trusts. With Authority set, the library fetches the issuer's public signing keys over HTTPS and refreshes them periodically, so the issuer can rotate keys without you redeploying.
  • ValidateIssuer and ValidateAudience together stop a token that is perfectly legitimate somewhere else being presented here. Without the audience check, a token your identity service issued for the internal reporting tool would be accepted by the orders API, carrying whatever claims the reporting tool needed.
  • ValidateLifetime enforces exp. ClockSkew tolerates small clock differences between the issuer and this server; the library's default is five minutes, which is more slack than most deployments need. Tightening it shortens the window in which a just-expired token still works, and in exchange requires your servers to keep reasonable time.
  • ValidAlgorithms pins the signing algorithm you accept, so a token is rejected unless it was signed the way you expect rather than however its own header claims it was signed.
  • Do not write your own version of this. The library's job is to get the order of these checks right and to fail closed when something is missing.

Two operations that look almost identical in code and answer completely different questions. Confusing them is the most consequential mistake in this lesson:

 DecodingValidating
What it doesBase64url-decodes the header and payload into JSONChecks the signature, then the issuer, audience, lifetime and algorithm
What it needsThe token, and nothing elseThe issuer's signing key, plus the values you expect to see
What it provesThat the token says somethingThat the token came from an issuer you trust, was meant for you, and is still current
Who can do itAnyone holding the token, including the userOnly a party with the signing key or its matching public key
Safe to act on the claims afterwardsNo. They are input, not factsYes, for as long as the token is valid
Typical useDebugging, or a client reading its own expiry to know when to refreshEvery single request the API handles

Expiry is the main control you have over a token that has ended up somewhere it should not be, which makes its value a design decision rather than a default to leave alone. A long-lived token is convenient and stays useful to whoever holds it. A short-lived one limits the exposure and makes the client renew more often.

The usual arrangement splits the job in two. An access token, measured in minutes, is sent with each request and is the one that travels widely. A refresh token, with a longer life, is held more carefully and used only against the identity service to obtain a new access token. Because refresh tokens are used rarely and in one place, they can be stored, tracked, rotated on each use, and revoked.

Revocation of access tokens is the genuinely hard part, and it is worth being precise about why. A validated token needs no database lookup — that is the property that makes the whole approach fast and lets several services accept the same credential. Revoking one means consulting a list on every request, which puts back exactly the lookup you removed. There is no clever trick that avoids this. There are only positions on it.

Three defensible positions: accept the gap and keep access tokens short, so a revoked employee loses access within minutes rather than instantly. Keep a deny list of revoked token identifiers in a fast cache and check it on each request, paying the lookup for the control. Or store one 'valid from' timestamp per account and reject tokens issued before it, which handles 'sign this person out everywhere' with a single cheap lookup rather than a growing list.

One consequence catches almost everyone: signing out in the browser deletes the client's copy of the token and nothing more. The token itself remains valid until it expires. Server-side sign-out means revoking the refresh token, and then living with the access token's remaining minutes — another reason to keep that number small.

Summary

  • A JWT has three parts: header, payload and signature, with the first two base64url-encoded rather than encrypted
  • Anyone holding a token can read its claims, so nothing confidential belongs in the payload
  • Validating means checking the signature, issuer, audience, lifetime and algorithm — decoding checks nothing
  • Keep access tokens short-lived and put the longer life on a refresh token that can be tracked and revoked
  • Revocation is hard because self-contained validation is exactly what makes tokens fast; choose your position on that trade deliberately

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

The salary band in the token

A colleague suggests adding each employee's salary band to the access token, to save a database lookup when rendering the internal directory.

What do you say, and what would you suggest instead?

Show solution

The payload is base64url-encoded, not encrypted, so the salary band is readable by the employee holding the token and by anyone who obtains a copy. A token turns up in browser storage, in HTTP client logs, in a proxy's records and in support screenshots. None of those are places to put pay data.

There is a second problem that has nothing to do with confidentiality: staleness. A claim is a snapshot from the moment of issue. A band corrected in the HR system does not change any token already issued, so the directory would show the old value until each token expired.

The alternative is the one the token was designed for: carry the account id, and look up anything sensitive or anything that changes on the server. If the lookup is genuinely a performance problem, cache it server-side, where you control who can read the cache.

Try it yourself

Find what the missing check allows

An orders API validates the signature and the expiry of incoming tokens, and has ValidateAudience set to false. All tokens come from your organisation's single identity service.

Write down what that configuration accepts that it should not, then correct the configuration.

Show solution

It accepts every token the identity service has ever issued, for any application, as long as it is unexpired. The internal reporting tool, the expenses app, a partner integration — each of those tokens is signed by the same issuer, so the signature check passes and the orders API treats the holder as a legitimate caller.

That matters because those tokens are issued in contexts with different rules. A token minted for a low-risk internal tool may be granted to people who have no orders permission at all, may live much longer, and may be handled with less care by the client that holds it.

The fix is two lines: ValidateAudience = true and ValidAudience set to this API's own identifier. The wider principle is that a signature answers 'who minted this', and only the audience check answers 'was it minted for me'.

C#
options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuerSigningKey = true,
    ValidateIssuer = true,
    ValidIssuer = "https://identity.example-company.com",
    ValidateAudience = true,
    ValidAudience = "orders-api",
    ValidateLifetime = true,
    ClockSkew = TimeSpan.FromSeconds(30),
};

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

What does the signature on a JWT give you?
Why is revoking an access token before it expires awkward?

Saved in this browser only.