Skip to main content
ANVISoftware Solutions
Lesson 15 of 18Advanced18 min

Authenticating API Callers

By the end of this lesson

Verify caller identity with tokens, and validate them correctly.

Authentication answers one question: who is calling? Authorisation answers a different one: may they do this? This lesson covers only the first, and the next lesson covers the second. Keeping them separate is not pedantry — most of the serious mistakes in this area come from an endpoint that answered the first question and assumed it had answered both.

The common mechanism for an API is a bearer token. The caller obtains a token from an identity provider, sends it on every request in the Authorization header, and your API decides whether to believe it. "Bearer" means possession is enough: whoever holds the token can use it, which is why tokens are short-lived and why they never belong in a URL or a log.

The parts of a token you have to care about:

Claims
Statements inside the token: who the subject is, which roles or scopes they have, when it was issued. Your API reads its caller's identity from these.
Issuer
Who created the token, identified by a URL. You accept tokens from the issuers you trust and no others.
Audience
Who the token is for. A token minted for a different API should not work on yours, and checking the audience is what stops it.
Expiry
When the token stops being valid. Short lifetimes limit the damage a leaked token can do, because it stops working on its own.
Signature
A cryptographic value over the token's contents, produced by the issuer. It is the only reason to believe any of the claims.
Signing keys
The public keys used to check the signature. An identity provider publishes them at a well-known address, and your framework fetches and caches them for you.

The most important sentence in this lesson: decoding a token is not validating it. A JWT is three base64url-encoded parts joined by dots. Base64url is an encoding, not encryption, so anyone holding the token can read every claim inside it with a text editor. Decoding tells you what the token says. It tells you nothing about whether the token is genuine.

Validation is the separate step that checks the signature against a key you trust, confirms the issuer and audience are ones you accept, and confirms the token is within its lifetime. Only after that do the claims mean anything. A handler that reads a user id out of an unvalidated token is reading a value the caller could have typed.

Two consequences follow. Never put anything confidential in a token, because its contents are readable by anyone who has it. And never take claims from a token your framework has not validated — which in practice means letting the authentication middleware do the work rather than parsing the header yourself.

Program.cs — validating bearer tokens
C#
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // Placeholders — real values come from configuration, never from source
        options.Authority = builder.Configuration["Auth:Authority"];
        options.Audience = builder.Configuration["Auth:Audience"];
        options.RequireHttpsMetadata = true;

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Auth:Authority"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Auth:Audience"],
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
  • Authority is the identity provider's address. From it the framework discovers the provider's public signing keys, fetches them, caches them and refreshes them when the provider rotates them, so no key material lives in your application.
  • ValidateIssuerSigningKey with ValidateIssuer is the pair that makes the token trustworthy: the signature checks out, and it was produced by the issuer you named.
  • ValidateAudience matters more than it looks. Without it, a valid token issued for a different API in the same organisation is accepted by yours — the signature is genuine, and it was never meant for you.
  • ClockSkew allows for clocks that disagree slightly between the issuer and your server. The default allowance is five minutes, which extends the life of an expired token by that much; thirty seconds is a common tightening.
  • UseAuthentication must come before UseAuthorization. The first establishes who the caller is; the second decides what they may do, and it has nothing to work with if the order is reversed.
  • RequireHttpsMetadata keeps key discovery on HTTPS. It is the default outside development, and it is worth stating so nobody turns it off by copying a development snippet.

What a correct validation step confirms. Skipping any one of these removes a real protection:

  • The signature verifies against a key from an issuer you trust
  • The issuer is on your accepted list, matched exactly
  • The audience names your API
  • The current time is inside the token's validity window, including its not-before time
  • The signing algorithm is one you expect, so a token cannot choose a weaker one
  • The token arrived over HTTPS, in the Authorization header — not in a query string, where it would reach logs and browser history
  • The token is short-lived, with renewal handled by the caller rather than by extending its lifetime

Summary

  • Authentication establishes who is calling; permission is a separate question
  • A bearer token is presented on every request, so it must be short-lived and kept out of URLs and logs
  • Decoding a token reads claims anyone can read or fabricate; validating it is what makes them trustworthy
  • Validate signature, issuer, audience and lifetime, and keep the framework's middleware in charge of it
  • Nothing confidential belongs inside a token, because its contents are readable by whoever holds it

Practice

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

Think about it

What decoding proves

A colleague says a request is authenticated because the code decoded the token and found a user id in it.

Explain what that decoding actually established, and what still needs to happen.

Show solution

Decoding established what the token claims, and nothing more. The payload is base64url-encoded rather than encrypted, so anyone can read it and anyone can produce a string that decodes to whatever claims they like.

What is missing is verification of the signature against a trusted key, plus the issuer, audience and lifetime checks. Until those pass, the user id is input from the caller.

The practical takeaway is to let the authentication middleware do this and read the resulting ClaimsPrincipal. Hand-decoding a token in a handler is the pattern that produces this mistake.

Think about it

Why the audience check matters

Your organisation runs three APIs behind the same identity provider. Someone suggests skipping the audience check, since every token comes from an issuer you trust.

Explain what that would allow, and why the issuer check is not a substitute.

Show solution

A token issued for one of the other APIs would then be accepted by yours. Its signature is genuine and its issuer is trusted, so both remaining checks pass — the only thing that would have stopped it is the audience.

The issuer check answers who minted the token. The audience check answers who it was minted for. They are different questions, and a shared identity provider is precisely the situation where the second one carries all the weight.

The consequence is that a caller holding a token for a lower-privilege API can present it to yours. Validating the audience keeps each token usable only where it was intended.

Knowledge check

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

Why is decoding a JWT not the same as validating it?
What does validating the audience protect against?

Saved in this browser only.