A runnable, debuggable OAuth 2.0 Authorization Code + PKCE example with multi-tenancy, scope enforcement, and JWT validation.
This repository includes:
- An Authorization Server
- A Single Page Application (SPA) Client
- A Resource API
The goal is clarity and observability, not production hardening. This project is designed to help you step through the full flow in a debugger and understand how the pieces fit together.
π Get the lab guide (PDF) β 27-page walkthrough with threat models, 10 attack scenarios, break/fix exercises, and a production checklist.
- OAuth 2.0 Authorization Code Flow
- PKCE (Proof Key for Code Exchange)
- Multi-tenancy concepts
- App registrations
- Scope issuance and enforcement
- JWT signing and validation
- Audience validation in a protected API
You can run this in two modes:
- Debug Mode (recommended for learning): run each service individually and set breakpoints.
- Docker Mode: run everything with one command.
Open three terminals in the repository root.
cd AuthServer
dotnet run
# Listening on: http://localhost:5001cd ResourceServer
dotnet run
# Listening on: http://localhost:5002cd WebClientServer
dotnet run
# Listening on: http://localhost:5003Then open:
http://localhost:5003
AuthServer/AuthorizationCodeHandler.cs: where the authorization code is created and where thecode_challengeis validatedAuthServer/JwtHandler.cs: where the JWT is generated and signedResourceServer/JwtValidator.cs: where the JWT is decoded andaud+scopeare validated
Step through the flow and observe the values moving between components.
docker compose up --build- Authorization Server: http://localhost:5001
- Resource API: http://localhost:5002
- SPA Client: http://localhost:5003
Browser SPA Client Auth Server Resource API
β β β β
β Click Login β β β
ββββββββββββββββββββββ>β β β
β β GET /authorize β β
β ββββββββββββββββββββββββ>β β
β β β stores code_challenge |
β β β β
β β<ββββββββββββββββββββββββ€ 302 redirect + code β
β β β β
β β POST /token β β
β ββββββββββββββββββββββββ>β validate code_verifier β
β β β issue JWT β
β β<ββββββββββββββββββββββββ€ access_token β
β β β β
β β GET /api/resource β β
β β Authorization: Bearer JWT β
β βββββββββββββββββββββββββββββββββββββββββββββββββ>β
β β β validate aud + scope β
β β<βββββββββββββββββββββββββββββββββββββββββββββββββ€
β Display data β β β
PKCE occurs in two steps:
- The SPA sends a
code_challenge(derived from acode_verifier) to/authorize. - The SPA later sends the original
code_verifierto/token, where the server recomputes and validates it.
These are the mechanics that matter.
- Inspect the request to
/authorize. - Note the
code_challengeandstateparameters. - In
AuthorizationCodeHandler.cs, observe how the challenge is stored.
- Inspect the POST to
/token. - Observe the
code_verifier. - The server hashes it and compares it to the stored
code_challenge. - If they do not match, the request is rejected.
-
Copy the
access_token. -
Decode it at https://jwt.io.
-
Observe claims such as:
aud(audience)scopesub(subject)iat,exp
- Request a token with different scopes (e.g.,
read,write). - Decode the JWT.
- Observe how scope claims appear.
- Watch how the Resource API checks them.
- Inspect
JwtValidator.cs. - The API validates that
audmatches its configured identifier. - This prevents tokens intended for one API from being used on another.
Modify the system and observe failures. This is where real understanding happens.
- Modify the redirect URI in the SPA request.
- The Authorization Server will reject it.
- Why: authorization codes are bound to registered redirect URIs.
- Change the
code_verifierbefore sending it to/token. - The server rejects the request.
- Why: PKCE prevents authorization code interception attacks.
- Request fewer scopes.
- Decode the JWT and observe missing claims.
- Call an endpoint requiring the removed scope.
- The API rejects it.
- Observe the
expclaim. - Wait until expiration or simulate clock drift.
- The API rejects the token.
- Modify the
audclaim during token generation. - Call the Resource API.
- The request fails audience validation.
- OAuth 2.0 (RFC 6749): https://www.rfc-editor.org/rfc/rfc6749
- PKCE (RFC 7636): https://www.rfc-editor.org/rfc/rfc7636
- JWT (RFC 7519): https://www.rfc-editor.org/rfc/rfc7519
- Microsoft Identity Platform β Authorization Code Flow: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
This repository is intentionally minimal and focused on clarity. It is not intended for direct production use without additional security hardening.