This project demonstrates a secure JWT access token + refresh token flow with token rotation using an ASP.NET Core Minimal API. Below is a clear step-by-step walkthrough of the code and runtime flow.
- Access Token (JWT) — short-lived token (e.g., 15 minutes). Sent in
Authorization: Bearer <token>for every protected API call. - Refresh Token — long-lived token (e.g., 7 days). Stored server-side and used only to obtain a new access token at
/auth/refresh. - Token Rotation — every time the refresh token is used, the server revokes the old refresh token and issues a new one. This reduces risk of replay attacks.
-
POST /auth/login- Purpose: Authenticate user and issue tokens.
- Body:
{ "username": "reena", "password": "password123" } - Response:
{ accessToken, refreshToken } - Server actions: validate credentials → create JWT → create RT → store RT in refresh store (in-memory demo).
-
POST /auth/refresh- Purpose: Exchange a valid refresh token for a new access token + new refresh token (rotation).
- Body:
{ "accessToken": "<expired_jwt>", "refreshToken": "<refresh_token>" } - Important: The refresh token is sent in the request body, not in headers or query string.
- Server actions: verify the RT from store (not expired, not revoked) → revoke old RT → create new RT → create new JWT → return both tokens.
-
GET /me(protected)- Purpose: Demonstrate a protected endpoint requiring JWT.
- Header:
Authorization: Bearer <accessToken> - Server actions: JWT middleware validates signature and
exp. If valid, request succeeds. If expired, middleware causes 401 and client should call/auth/refresh.
- The existing JWT cannot be modified after it is issued (immutable).
- When a JWT expires, the server does not edit the expired JWT; it generates a completely new JWT (new signature and new
expvalue). This is how immutability and refresh flow coexist.
- Do not store refresh tokens in plaintext — store a hashed value and compare hashes on lookup.
- Mark the old refresh token as
IsRevoked = trueandIsUsed = truewhen rotating. - Bind refresh tokens to device/IP metadata when possible to detect anomalies.
- Rate-limit and monitor the refresh endpoint for abnormal usage (replay detection).
- Always use HTTPS in production and rotate signing keys periodically.
- Clone repo
git clone https://github.com/<your-username>/JwtRefreshDemo.git cd JwtRefreshDemo
Run
Copy code dotnet restore dotnet run --urls "http://localhost:7219" Test flow:
POST /auth/login → get accessToken & refreshToken
GET /me with header Authorization: Bearer (works while token not expired)

After token expiry (or simulate by using an expired token), call POST /auth/refresh with body { "accessToken": "", "refreshToken": "" } to get new tokens.

- Files & structure (quick) Program.cs — minimal API with endpoints and DI registrations.
TokenService — token generation (access + refresh).
InMemoryRefreshTokenStore — demo store (replace with EF Core / persistent store in production).
InMemoryUserRepository — demo user store (replace with real user DB).
I just published a hands-on demo showing a secure JWT + Refresh Token flow implemented in ASP.NET Core (Minimal API). The project illustrates token rotation, refresh endpoint best practices, and how to safely renew access tokens without forcing users to log in again.
Highlights: • Short-lived JWT for API calls (Authorization header) • Long-lived Refresh Token stored server-side (sent in request body only) • Token rotation and revocation to prevent replay attacks • In-memory demo ready to extend to EF Core and production deployments
Explore the code and step-by-step README: If you want, I can walk you through the code or add an Angular demo for client-side flow. #dotnet #webapi #security #jwt
What you’ll learn: • How the server validates JWT (exp claim and signature) and why expired tokens cause 401 • Why refresh tokens must be sent only in the refresh request body and validated server-side • How token rotation works — revoke old refresh tokens and issue new ones to avoid replay attacks • Production hardening tips: hash refresh tokens in DB, bind tokens to device/IP metadata, rate-limit the refresh endpoint, and always use HTTPS