# API Authentication

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: Production-Ready APIs
Source: https://www.devtutorials.ai/learn/building-apis/production-ready/authentication

> Make the API ask who is calling: protect endpoints with API keys, read and validate JWT bearer tokens, and answer 401 with WWW-Authenticate.

**Authentication** answers one simple question: **Who is calling?**

Before an API returns private information or performs an action, it must verify the identity of the caller. The caller may prove their identity using an API key, a token, or another authentication method.

Authentication happens before the API decides what the caller is allowed to do. That second step is called **authorization**, and it is covered in a later lesson.

## Watch Anyone Read Every Order

The Brew & Bytes API has an endpoint that returns orders. There is one problem: it does not check who is calling. Press **Run**.

The request sends no API key, no token, and no credentials. Yet the API still returns the customer ids, the order totals, and the order information. Anyone who discovers the URL can access the data.

This is one of the most common API security mistakes.

**In simple words:** the API never verified the identity of the caller. In this lesson, you will learn how to require authentication before returning protected data.

## TL;DR

- **Authentication** answers **Who are you?**
- **Authorization** answers **What are you allowed to do?**
- **API keys** are secret values often used for server-to-server communication.
- **JWTs (JSON Web Tokens)** are commonly used to authenticate users.
- A JWT is sent in this header: `Authorization: Bearer <token>`
- A JWT contains a header, a payload, and a signature.
- Anyone can read the header and payload.
- The signature proves the token has not been changed.
- Every request should validate the signature, the expiration (`exp`), and the audience (`aud`).
- Missing or invalid credentials should return `401 Unauthorized`, with an appropriate `WWW-Authenticate` header.

## A Real-World Example

Think about entering an office building. A security guard checks your ID card before allowing access.

The ID card includes your name, your photo, an expiry date, and security features that are difficult to fake. The guard does not call headquarters every time someone enters. Instead, they verify that the card is genuine and that the card has not expired.

JWTs work in a similar way. The token contains information about the user, and the API verifies that the token is valid before trusting it.

## What You Will Learn

In this lesson, you will learn:

1. The difference between authentication and authorization.
2. How API keys work.
3. What information a JWT contains.
4. How JWT validation works.
5. Why token signatures matter.
6. How OAuth 2.0 and PKCE support secure sign-in.

## Authentication vs Authorization

These concepts are closely related but solve different problems.

| Question | Concept |
|---|---|
| Who is calling? | Authentication |
| What may they do? | Authorization |

Examples: a valid token means authentication succeeds. If a staff permission is required, authorization decides whether access is allowed. A missing token returns `401 Unauthorized`. A valid token with insufficient permission returns `403 Forbidden`.

## API Keys

API keys are commonly used when one server talks to another server. A partner receives a secret key and sends it with each request.

Example: `X-Api-Key: bb_live_7f3a...`

Results:

### No Key

The response is `401 Unauthorized` with `WWW-Authenticate: ApiKey header="X-Api-Key"`.

### Invalid Key

The response is `401 Unauthorized`.

### Valid Key

The response is `200 OK`. The request succeeds.

Never put API keys inside browser applications or mobile apps. Users can pull them out of the app and use them somewhere else. API keys are only for trusted server-to-server communication.

## Inside a JWT

A **JSON Web Token (JWT)** contains three parts that are separated by periods: `header.payload.signature`

### Header

The header contains metadata about the token. Example: `{"alg": "HS256", "typ": "JWT"}`

### Payload

The payload contains claims. Example: `{"sub": "customer-7", "aud": "brew-and-bytes-api", "exp": 1780000000}`

Important claims include:

| Claim | Purpose |
|---|---|
| `sub` | User identifier |
| `aud` | Intended API |
| `exp` | Expiration time |
| `iss` | Token issuer |
| `role` | User role |

### Signature

The signature proves the token was issued by a trusted authority and has not been changed.

### Important

Anyone who has the token can read the header and payload. Never store passwords, API secrets, or sensitive private information inside a JWT.

## Validate the Token

The API should validate every bearer token before processing a protected request. The validation process follows four steps.

### 1. Read the Token

Read the header: `Authorization: Bearer eyJ...`

### 2. Verify the Signature

The API recalculates the signature and compares it with the token.

### 3. Check Expiration

Verify that the time in `"exp": 1780000000` has not passed.

### 4. Check Audience

Verify that `"aud": "brew-and-bytes-api"` matches your API.

Results:

| Token State | Result |
|---|---|
| Valid token | Access granted |
| Expired token | `401 Unauthorized` |
| Invalid audience | `401 Unauthorized` |
| Invalid signature | `401 Unauthorized` |
| Missing token | `401 Unauthorized` |

Each failure should explain the problem using the `WWW-Authenticate` header.

In real ASP.NET Core applications, JWT validation is usually handled for you with this setup: builder.Services.AddAuthentication().AddJwtBearer(); and this call on protected endpoints: .RequireAuthorization(). The framework does the same validation steps for you.

## How Token Signing Works

When a user signs in, a trusted server creates the token. It creates a header, creates a payload, signs both using a secret or private key, and produces a signature. The token is then returned to the client.

Even a tiny change produces a completely different signature. For example, `{"sub": "customer-7"}` and `{"sub": "customer-8"}` generate different signatures.

Because the signature depends on the content, users cannot change claims without making the token invalid.

Store signing secrets in a safe place. Use configuration files, environment variables, or secret-management services. Do not hard-code secrets in source code.

## Signing In with OAuth 2.0 and PKCE

Most modern sign-in systems use **OAuth 2.0 Authorization Code Flow with PKCE**. Instead of handling passwords directly:

1. The user signs in through an identity provider.
2. The identity provider returns an authorization code.
3. The application exchanges the code for tokens.

PKCE adds extra protection by checking that the application that exchanges the code is the same application that started the sign-in process.

Important parts:

### Code Verifier

A secret value that is generated by the client.

### Code Challenge

A hash of the verifier: `BASE64URL(SHA256(verifier))`

### State

A random value that prevents request forgery. Example: `state=xyz123`

### Token Exchange

The authorization server validates the verifier before issuing tokens. If the verifier does not match, this error is returned: `{"error": "invalid_grant"}`

The stolen authorization code becomes useless.

Do not build your own authentication system or identity provider. Use a trusted provider such as Microsoft Entra ID, Auth0, Keycloak, or another established identity platform.

## Where You See This

- Mobile apps authenticate users with bearer tokens.
- Web applications use OAuth 2.0 and PKCE for sign-in.
- Partner integrations often use API keys.
- Internal services may use machine-to-machine tokens.
- Public APIs protect user and business data through token validation.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| Anyone can view private data | Authentication is missing | Require tokens or API keys |
| Expired tokens continue to work | Expiration is not checked | Validate `exp` |
| Tokens issued for another API are accepted | Audience is not checked | Validate `aud` |
| Modified tokens are accepted | Signature is not verified | Validate the signature |
| Stolen authorization codes can be exchanged | OAuth flow does not use PKCE | Use PKCE and verify the code verifier |
| API keys are discovered by users | Keys are embedded in client apps | Move keys to server-side code only |

## Exercises

## What to Remember

- Every endpoint with private data must ask **who is calling**.
- **API keys** are for servers. **JWTs** are for the apps of users.
- The payload of a JWT can be read by anyone. The **signature** proves that it is real.
- Check **signature**, **expiry** and **audience** on every request.
- Missing or bad credentials: `401` with `WWW-Authenticate`.

**Next:** API Authorization and Security. You will decide what each caller is allowed to do, and when to answer `403 Forbidden`.

var app = builder.Build();

var orders = new List<Order> { new(101, "customer-7", "Latte", 440) };

// No check at all. Anyone who knows the URL can read every order.
app.MapGet("/orders", () => Results.Ok(orders));

app.Run();

record Order(int Id, string CustomerId, string Item, int Total);`;

type Order = { id: number; customerId: string; item: string; total: number };
const orders: Order[] = [{ id: 101, customerId: "customer-7", item: "Latte", total: 440 }];

const app = new Hono();

// No check at all. Anyone who knows the URL can read every order.
app.get("/orders", (c) => c.json(orders, 200));

var app = builder.Build();

// Each partner gets its own secret key. The key says which partner is calling.
var apiKeys = new Dictionary<string, string> { ["dk_live_swiggo_7f3a"] = "Swiggo Delivery" };

app.MapGet("/partner/orders", (HttpContext context) =>
{
    string key = context.Request.Headers["X-Api-Key"].ToString();
    if (!apiKeys.TryGetValue(key, out var partner))
    {
        context.Response.Headers["WWW-Authenticate"] = "ApiKey header=\\"X-Api-Key\\"";
        return Results.StatusCode(401);
    }
    return Results.Ok(new { partner, orders = new[] { 101, 102 } });
});

app.Run();`;

// Each partner gets its own secret key. The key says which partner is calling.
const apiKeys = new Map<string, string>([["dk_live_swiggo_7f3a", "Swiggo Delivery"]]);

const app = new Hono();

app.get("/partner/orders", (c) => {
  const key = c.req.header("X-Api-Key") ?? "";
  const partner = apiKeys.get(key);
  if (!partner) {
    c.header("WWW-Authenticate", 'ApiKey header="X-Api-Key"');
    return c.body(null, 401);
  }
  return c.json({ partner, orders: [101, 102] }, 200);
});

###
GET /partner/orders
X-Api-Key: guess-123

###
GET /partner/orders
X-Api-Key: dk_live_swiggo_7f3a`;

// A JWT is three parts joined by dots: header.payload.signature
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

string[] parts = token.Split('.');

// The first two parts are just base64url-encoded JSON. Anyone can read them.
static string Decode(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    b64 = b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '=');
    return Encoding.UTF8.GetString(Convert.FromBase64String(b64));
}

Console.WriteLine("Header:    " + Decode(parts[0]));
Console.WriteLine("Payload:   " + Decode(parts[1]));
Console.WriteLine("Signature: " + parts[2]);`;

const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

const parts = token.split(".");

// The first two parts are just base64url-encoded JSON. Anyone can read them.
function decode(part: string): string {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  return new TextDecoder().decode(Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0)));
}

console.log("Header:    " + decode(parts[0]));
console.log("Payload:   " + decode(parts[1]));
console.log("Signature: " + parts[2]);`;

using System.Text;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

const string Secret = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const string Audience = "brew-and-bytes-api";                 // tokens must be made for this API
const long Now = 1790000000;                                   // a fixed "now", so this page always gives the same answer

// Returns null when the token is good, or the reason it is not.
string? Check(string token)
{
    string[] parts = token.Split('.');
    if (parts.Length != 3) return "The token is not a JWT";

    // 1. Signature: recompute it with the secret. Any change to the token breaks it.
    byte[] expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
    if (!CryptographicOperations.FixedTimeEquals(expected, FromBase64Url(parts[2]))) return "The signature is invalid";

    using var payload = JsonDocument.Parse(FromBase64Url(parts[1]));
    // 2. Expiry: a token only works until "exp".
    if (payload.RootElement.GetProperty("exp").GetInt64() <= Now) return "The token expired";
    // 3. Audience: a token made for another API is not for us.
    if (payload.RootElement.GetProperty("aud").GetString() != Audience) return "The audience is invalid";
    return null;
}

app.MapGet("/orders/mine", (HttpContext context) =>
{
    string header = context.Request.Headers["Authorization"].ToString();
    if (!header.StartsWith("Bearer "))
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    string? problem = Check(header["Bearer ".Length..]);
    if (problem is not null)
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer error=\\"invalid_token\\", error_description=\\"" + problem + "\\"";
        return Results.StatusCode(401);
    }
    return Results.Ok(new[] { new { id = 101, item = "Latte", total = 440 } });
});

app.Run();

static byte[] FromBase64Url(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    return Convert.FromBase64String(b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '='));
}`;

const SECRET = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const AUDIENCE = "brew-and-bytes-api"; // tokens must be made for this API
const NOW = 1790000000; // a fixed "now", so this page always gives the same answer

function fromBase64Url(part: string): Uint8Array {
  return Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (ch) => ch.charCodeAt(0));
}

// Returns null when the token is good, or the reason it is not.
async function check(token: string): Promise<string | null> {
  const parts = token.split(".");
  if (parts.length !== 3) return "The token is not a JWT";

  // 1. Signature: verify it with the secret. Any change to the token breaks it.
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const ok = await crypto.subtle.verify("HMAC", key, fromBase64Url(parts[2]), new TextEncoder().encode(parts[0] + "." + parts[1]));
  if (!ok) return "The signature is invalid";

  const payload = JSON.parse(new TextDecoder().decode(fromBase64Url(parts[1])));
  // 2. Expiry: a token only works until "exp".
  if (payload.exp <= NOW) return "The token expired";
  // 3. Audience: a token made for another API is not for us.
  if (payload.aud !== AUDIENCE) return "The audience is invalid";
  return null;
}

const app = new Hono();

app.get("/orders/mine", async (c) => {
  const header = c.req.header("Authorization") ?? "";
  if (!header.startsWith("Bearer ")) {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  const problem = await check(header.slice("Bearer ".length));
  if (problem) {
    c.header("WWW-Authenticate", 'Bearer error="invalid_token", error_description="' + problem + '"');
    return c.body(null, 401);
  }
  return c.json([{ id: 101, item: "Latte", total: 440 }], 200);
});

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk

###
GET /orders/mine

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzg5OTkwMDAwfQ.Q7zneQ5qVOG7mCZg7AZCpKja7DG7UfBRLprk6_Rg-5I

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci04IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoicGFydG5lci1hcGkiLCJleHAiOjE3OTAwMDM2MDB9.jMLrdQpd4MksCMI1dRgylAAz6zIYPyRlWN_O3osCC0Y`;

using System.Text;

// The sign-in server makes a token after the customer logs in.
const string Secret = "brew-and-bytes-demo-secret-change-me";

static string Base64Url(byte[] bytes) =>
    Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');

string Sign(string headerJson, string payloadJson)
{
    string head = Base64Url(Encoding.UTF8.GetBytes(headerJson));
    string body = Base64Url(Encoding.UTF8.GetBytes(payloadJson));
    byte[] signature = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.UTF8.GetBytes(head + "." + body));
    return head + "." + body + "." + Base64Url(signature);
}

string header = """{"alg":"HS256","typ":"JWT"}""";
string token = Sign(header, """{"sub":"customer-7","name":"Asha Rao","aud":"brew-and-bytes-api","exp":1790003600}""");
string other = Sign(header, """{"sub":"customer-8","name":"Asha Rao","aud":"brew-and-bytes-api","exp":1790003600}""");

Console.WriteLine("Token for customer 7:");
Console.WriteLine(token);
Console.WriteLine();
Console.WriteLine("Signature for customer 7: " + token.Split('.')[2]);
Console.WriteLine("Signature for customer 8: " + other.Split('.')[2]);
Console.WriteLine("Change one letter in the payload, and the signature is completely different.");`;

const SECRET = "brew-and-bytes-demo-secret-change-me";

function base64Url(bytes: Uint8Array): string {
  return btoa(String.fromCharCode(...bytes)).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");
}

async function sign(headerJson: string, payloadJson: string): Promise<string> {
  const head = base64Url(new TextEncoder().encode(headerJson));
  const body = base64Url(new TextEncoder().encode(payloadJson));
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const signature = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(head + "." + body)));
  return head + "." + body + "." + base64Url(signature);
}

const header = '{"alg":"HS256","typ":"JWT"}';
const token = await sign(header, '{"sub":"customer-7","name":"Asha Rao","aud":"brew-and-bytes-api","exp":1790003600}');
const other = await sign(header, '{"sub":"customer-8","name":"Asha Rao","aud":"brew-and-bytes-api","exp":1790003600}');

console.log("Token for customer 7:");
console.log(token);
console.log("");
console.log("Signature for customer 7: " + token.split(".")[2]);
console.log("Signature for customer 8: " + other.split(".")[2]);
console.log("Change one letter in the payload, and the signature is completely different.");`;

using System.Text;

string Base64Url(byte[] bytes) => Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
string Challenge(string verifier) => Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier)));

// 1. The app makes a secret "code verifier" and keeps it. (Real apps use random bytes.)
var verifier = "brew-and-bytes-demo-verifier-0123456789-abcdefghij";
// 2. It sends only the hash of the verifier, called the "code challenge".
var challenge = Challenge(verifier);
Console.WriteLine("Verifier:  " + verifier);
Console.WriteLine("Challenge: " + challenge);

// 3. The app opens the sign-in page with this URL.
var url = "https://login.example.com/authorize?response_type=code&client_id=brew-app"
    + "&redirect_uri=" + Uri.EscapeDataString("https://app.example.com/callback")
    + "&scope=" + Uri.EscapeDataString("orders.read") + "&state=xyz123"
    + "&code_challenge=" + challenge + "&code_challenge_method=S256";
Console.WriteLine("Authorize: " + url);

// 4. The user signs in. The sign-in server remembers the challenge and returns a code.
var saved = new Dictionary<string, string> { ["code-555"] = challenge };
Console.WriteLine("Redirect:  https://app.example.com/callback?code=code-555&state=xyz123");

// 5. The app swaps the code plus the verifier for a token. The server hashes the verifier and compares.
string Token(string code, string v) =>
    saved.TryGetValue(code, out var c) && c == Challenge(v)
        ? "{\\"access_token\\":\\"at-asha-7\\",\\"token_type\\":\\"Bearer\\",\\"expires_in\\":3600}"
        : "{\\"error\\":\\"invalid_grant\\"}";

Console.WriteLine("Right verifier: " + Token("code-555", verifier));
Console.WriteLine("Wrong verifier: " + Token("code-555", "stolen-code-but-no-verifier"));`;

  btoa(String.fromCharCode(...bytes)).replace(/=+$/, "").replace(/\\+/g, "-").replace(/\\//g, "_");
const challengeOf = async (verifier: string) =>
  base64Url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));

// 1. The app makes a secret "code verifier" and keeps it. (Real apps use random bytes.)
const verifier = "brew-and-bytes-demo-verifier-0123456789-abcdefghij";
// 2. It sends only the hash of the verifier, called the "code challenge".
const challenge = await challengeOf(verifier);
console.log("Verifier:  " + verifier);
console.log("Challenge: " + challenge);

// 3. The app opens the sign-in page with this URL.
const url = "https://login.example.com/authorize?response_type=code&client_id=brew-app"
  + "&redirect_uri=" + encodeURIComponent("https://app.example.com/callback")
  + "&scope=" + encodeURIComponent("orders.read") + "&state=xyz123"
  + "&code_challenge=" + challenge + "&code_challenge_method=S256";
console.log("Authorize: " + url);

// 4. The user signs in. The sign-in server remembers the challenge and returns a code.
const saved = new Map<string, string>([["code-555", challenge]]);
console.log("Redirect:  https://app.example.com/callback?code=code-555&state=xyz123");

// 5. The app swaps the code plus the verifier for a token. The server hashes the verifier and compares.
async function token(code: string, v: string): Promise<string> {
  return saved.get(code) === (await challengeOf(v))
    ? '{"access_token":"at-asha-7","token_type":"Bearer","expires_in":3600}'
    : '{"error":"invalid_grant"}';
}

console.log("Right verifier: " + (await token("code-555", verifier)));
console.log("Wrong verifier: " + (await token("code-555", "stolen-code-but-no-verifier")));`;

  {
    prompt: "Decode the payload of the token, and print its sub and aud claims.",
    hint: "Split on dots, take the second part, decode Base64URL, and then parse the JSON.",
    explain: "Anyone can read the payload of a JWT. That is why it must never hold secrets.",
    csharp: { starter: `using System.Text;

// A JWT is three parts joined by dots: header.payload.signature
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

string[] parts = token.Split('.');

// The first two parts are just base64url-encoded JSON. Anyone can read them.
static string Decode(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    b64 = b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '=');
    return Encoding.UTF8.GetString(Convert.FromBase64String(b64));
}

// Read the payload and print two lines:
// sub: customer-7
// aud: brew-and-bytes-api
Console.WriteLine(token);`, solution: `using System.Text;

// A JWT is three parts joined by dots: header.payload.signature
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

string[] parts = token.Split('.');

// The first two parts are just base64url-encoded JSON. Anyone can read them.
static string Decode(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    b64 = b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '=');
    return Encoding.UTF8.GetString(Convert.FromBase64String(b64));
}

// Read the payload and print two lines:
// sub: customer-7
// aud: brew-and-bytes-api
using var payload = System.Text.Json.JsonDocument.Parse(Decode(parts[1]));
Console.WriteLine("sub: " + payload.RootElement.GetProperty("sub").GetString());
Console.WriteLine("aud: " + payload.RootElement.GetProperty("aud").GetString());`, mode: "console" },
    react: { starter: `// A JWT is three parts joined by dots: header.payload.signature
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

const parts = token.split(".");

// The first two parts are just base64url-encoded JSON. Anyone can read them.
function decode(part: string): string {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  return new TextDecoder().decode(Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0)));
}

// Read the payload and print two lines:
// sub: customer-7
// aud: brew-and-bytes-api
console.log(token);`, solution: `// A JWT is three parts joined by dots: header.payload.signature
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk";

const parts = token.split(".");

// The first two parts are just base64url-encoded JSON. Anyone can read them.
function decode(part: string): string {
  const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
  return new TextDecoder().decode(Uint8Array.from(atob(b64), (ch) => ch.charCodeAt(0)));
}

// Read the payload and print two lines:
// sub: customer-7
// aud: brew-and-bytes-api
const payload = JSON.parse(decode(parts[1]));
console.log("sub: " + payload.sub);
console.log("aud: " + payload.aud);`, mode: "console" }
  },
  {
    prompt: "Protect GET /orders: only \"Authorization: Bearer demo-token-7\" may read it. Anything else gets 401 with WWW-Authenticate: Bearer.",
    hint: "Read the Authorization header and compare it. If it does not match, return 401 and set the header.",
    explain: "No valid credentials means 401, and WWW-Authenticate tells the caller how to authenticate.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Only callers with "Authorization: Bearer demo-token-7" may read orders.
// Anyone else gets 401 with the header WWW-Authenticate: Bearer
app.MapGet("/orders", (HttpContext context) =>
{
    return Results.Ok(new[] { new { id = 101, item = "Latte" } });
});

app.Run();`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Only callers with "Authorization: Bearer demo-token-7" may read orders.
// Anyone else gets 401 with the header WWW-Authenticate: Bearer
app.MapGet("/orders", (HttpContext context) =>
{
    if (context.Request.Headers["Authorization"].ToString() != "Bearer demo-token-7")
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    return Results.Ok(new[] { new { id = 101, item = "Latte" } });
});

app.Run();`, mode: "aspnet", requests: `GET /orders

###
GET /orders
Authorization: Bearer demo-token-7` },
    react: { starter: `import { Hono } from "hono";

const app = new Hono();

// Only callers with "Authorization: Bearer demo-token-7" may read orders.
// Anyone else gets 401 with the header WWW-Authenticate: Bearer
app.get("/orders", (c) => {
  return c.json([{ id: 101, item: "Latte" }], 200);
});

const app = new Hono();

// Only callers with "Authorization: Bearer demo-token-7" may read orders.
// Anyone else gets 401 with the header WWW-Authenticate: Bearer
app.get("/orders", (c) => {
  if (c.req.header("Authorization") !== "Bearer demo-token-7") {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  return c.json([{ id: 101, item: "Latte" }], 200);
});

###
GET /orders
Authorization: Bearer demo-token-7` },
    checks: `GET /orders
Authorization: Bearer demo-token-8

###
GET /orders
Authorization: demo-token-7

###
GET /orders
Authorization: Bearer demo-token-7`
  },
  {
    prompt: "The validator checks the signature and the audience, but an expired token still works. Add the expiry check.",
    hint: "Compare the exp claim with Now. If exp is not in the future, return \"The token expired\".",
    explain: "Without an expiry check, a stolen token works forever.",
    csharp: { starter: `using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

const string Secret = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const string Audience = "brew-and-bytes-api";                 // tokens must be made for this API
const long Now = 1790000000;                                   // a fixed "now", so this page always gives the same answer

// Returns null when the token is good, or the reason it is not.
// The expiry check is missing: an expired token still works. Reject it with
// the reason "The token expired" when "exp" is not after Now.
string? Check(string token)
{
    string[] parts = token.Split('.');
    if (parts.Length != 3) return "The token is not a JWT";

    // 1. Signature: recompute it with the secret. Any change to the token breaks it.
    byte[] expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
    if (!CryptographicOperations.FixedTimeEquals(expected, FromBase64Url(parts[2]))) return "The signature is invalid";

    using var payload = JsonDocument.Parse(FromBase64Url(parts[1]));
    // 3. Audience: a token made for another API is not for us.
    if (payload.RootElement.GetProperty("aud").GetString() != Audience) return "The audience is invalid";
    return null;
}

app.MapGet("/orders/mine", (HttpContext context) =>
{
    string header = context.Request.Headers["Authorization"].ToString();
    if (!header.StartsWith("Bearer "))
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    string? problem = Check(header["Bearer ".Length..]);
    if (problem is not null)
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer error=\\"invalid_token\\", error_description=\\"" + problem + "\\"";
        return Results.StatusCode(401);
    }
    return Results.Ok(new[] { new { id = 101, item = "Latte", total = 440 } });
});

app.Run();

static byte[] FromBase64Url(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    return Convert.FromBase64String(b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '='));
}`, solution: `using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

const string Secret = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const string Audience = "brew-and-bytes-api";                 // tokens must be made for this API
const long Now = 1790000000;                                   // a fixed "now", so this page always gives the same answer

// Returns null when the token is good, or the reason it is not.
// The expiry check is missing: an expired token still works. Reject it with
// the reason "The token expired" when "exp" is not after Now.
string? Check(string token)
{
    string[] parts = token.Split('.');
    if (parts.Length != 3) return "The token is not a JWT";

    // 1. Signature: recompute it with the secret. Any change to the token breaks it.
    byte[] expected = HMACSHA256.HashData(Encoding.UTF8.GetBytes(Secret), Encoding.UTF8.GetBytes(parts[0] + "." + parts[1]));
    if (!CryptographicOperations.FixedTimeEquals(expected, FromBase64Url(parts[2]))) return "The signature is invalid";

    using var payload = JsonDocument.Parse(FromBase64Url(parts[1]));
    // 2. Expiry: a token only works until "exp".
    if (payload.RootElement.GetProperty("exp").GetInt64() <= Now) return "The token expired";
    // 3. Audience: a token made for another API is not for us.
    if (payload.RootElement.GetProperty("aud").GetString() != Audience) return "The audience is invalid";
    return null;
}

app.MapGet("/orders/mine", (HttpContext context) =>
{
    string header = context.Request.Headers["Authorization"].ToString();
    if (!header.StartsWith("Bearer "))
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer";
        return Results.StatusCode(401);
    }
    string? problem = Check(header["Bearer ".Length..]);
    if (problem is not null)
    {
        context.Response.Headers["WWW-Authenticate"] = "Bearer error=\\"invalid_token\\", error_description=\\"" + problem + "\\"";
        return Results.StatusCode(401);
    }
    return Results.Ok(new[] { new { id = 101, item = "Latte", total = 440 } });
});

app.Run();

static byte[] FromBase64Url(string part)
{
    string b64 = part.Replace('-', '+').Replace('_', '/');
    return Convert.FromBase64String(b64.PadRight(b64.Length + (4 - b64.Length % 4) % 4, '='));
}`, mode: "aspnet", requests: `GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzg5OTkwMDAwfQ.Q7zneQ5qVOG7mCZg7AZCpKja7DG7UfBRLprk6_Rg-5I

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk` },
    react: { starter: `import { Hono } from "hono";

const SECRET = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const AUDIENCE = "brew-and-bytes-api"; // tokens must be made for this API
const NOW = 1790000000; // a fixed "now", so this page always gives the same answer

function fromBase64Url(part: string): Uint8Array {
  return Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (ch) => ch.charCodeAt(0));
}

// Returns null when the token is good, or the reason it is not.
// The expiry check is missing: an expired token still works. Reject it with
// the reason "The token expired" when "exp" is not after Now.
async function check(token: string): Promise<string | null> {
  const parts = token.split(".");
  if (parts.length !== 3) return "The token is not a JWT";

  // 1. Signature: verify it with the secret. Any change to the token breaks it.
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const ok = await crypto.subtle.verify("HMAC", key, fromBase64Url(parts[2]), new TextEncoder().encode(parts[0] + "." + parts[1]));
  if (!ok) return "The signature is invalid";

  const payload = JSON.parse(new TextDecoder().decode(fromBase64Url(parts[1])));
  // 3. Audience: a token made for another API is not for us.
  if (payload.aud !== AUDIENCE) return "The audience is invalid";
  return null;
}

const app = new Hono();

app.get("/orders/mine", async (c) => {
  const header = c.req.header("Authorization") ?? "";
  if (!header.startsWith("Bearer ")) {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  const problem = await check(header.slice("Bearer ".length));
  if (problem) {
    c.header("WWW-Authenticate", 'Bearer error="invalid_token", error_description="' + problem + '"');
    return c.body(null, 401);
  }
  return c.json([{ id: 101, item: "Latte", total: 440 }], 200);
});

const SECRET = "brew-and-bytes-demo-secret-change-me"; // shared with the sign-in server
const AUDIENCE = "brew-and-bytes-api"; // tokens must be made for this API
const NOW = 1790000000; // a fixed "now", so this page always gives the same answer

function fromBase64Url(part: string): Uint8Array {
  return Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (ch) => ch.charCodeAt(0));
}

// Returns null when the token is good, or the reason it is not.
// The expiry check is missing: an expired token still works. Reject it with
// the reason "The token expired" when "exp" is not after Now.
async function check(token: string): Promise<string | null> {
  const parts = token.split(".");
  if (parts.length !== 3) return "The token is not a JWT";

  // 1. Signature: verify it with the secret. Any change to the token breaks it.
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(SECRET), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  const ok = await crypto.subtle.verify("HMAC", key, fromBase64Url(parts[2]), new TextEncoder().encode(parts[0] + "." + parts[1]));
  if (!ok) return "The signature is invalid";

  const payload = JSON.parse(new TextDecoder().decode(fromBase64Url(parts[1])));
  // 2. Expiry: a token only works until "exp".
  if (payload.exp <= NOW) return "The token expired";
  // 3. Audience: a token made for another API is not for us.
  if (payload.aud !== AUDIENCE) return "The audience is invalid";
  return null;
}

const app = new Hono();

app.get("/orders/mine", async (c) => {
  const header = c.req.header("Authorization") ?? "";
  if (!header.startsWith("Bearer ")) {
    c.header("WWW-Authenticate", "Bearer");
    return c.body(null, 401);
  }
  const problem = await check(header.slice("Bearer ".length));
  if (problem) {
    c.header("WWW-Authenticate", 'Bearer error="invalid_token", error_description="' + problem + '"');
    return c.body(null, 401);
  }
  return c.json([{ id: 101, item: "Latte", total: 440 }], 200);
});

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzg5OTkwMDAwfQ.Q7zneQ5qVOG7mCZg7AZCpKja7DG7UfBRLprk6_Rg-5I

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoiYnJldy1hbmQtYnl0ZXMtYXBpIiwiZXhwIjoxNzkwMDAzNjAwfQ.cMQBRHX5CbBVOtMYrzse5vO2KuaAWS09skapxIxmvVk` },
    checks: `GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci04IiwibmFtZSI6IlJhdmkiLCJhdWQiOiJicmV3LWFuZC1ieXRlcy1hcGkiLCJleHAiOjE3OTAwMDAwMDB9.dnnhfjCsabTbieCDyyeOTzTxaMacw79raMeFMalrOag

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci04IiwibmFtZSI6IlJhdmkiLCJhdWQiOiJicmV3LWFuZC1ieXRlcy1hcGkiLCJleHAiOjE3OTAwODY0MDB9.v69WMgRPcfcTQ0iTUxJow5b0ZV6wUKyIHQ2nkFV-uFg

###
GET /orders/mine
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXN0b21lci03IiwibmFtZSI6IkFzaGEgUmFvIiwiYXVkIjoibG95YWx0eS1hcGkiLCJleHAiOjE3OTAwMDM2MDB9.RR9wbqGSgYbFEBdYlwQuxHhGGZmzndOhiV_ek5FiLTY`
  }
];
