|
| 1 | +# ASP.NET Core API Security — Minimal JWT and Rate Limiting Sample |
| 2 | + |
| 3 | +Full tutorial: [ASP.NET Core 8 API Security: JWT Authentication, CSRF Protection & Rate Limiting](https://www.dotnet-guide.com/tutorials/aspnet-core/api-security-in-practice/) |
| 4 | + |
| 5 | +## What this sample demonstrates |
| 6 | + |
| 7 | +- .NET 10 ASP.NET Core Minimal API |
| 8 | +- JWT bearer authentication with issuer, audience, lifetime, signing-key, and algorithm validation |
| 9 | +- Short-lived tokens (30 minutes) |
| 10 | +- Development-only login endpoint (`POST /auth/token`) |
| 11 | +- One seeded demo user |
| 12 | +- One protected Notes resource with ownership checks |
| 13 | +- 404 for another user's hidden note (anti-enumeration) |
| 14 | +- Login rate limiting (5 requests/minute) |
| 15 | +- Per-user API rate limiting (20 requests/minute) |
| 16 | +- JSON 401 and 429 responses following RFC-style |
| 17 | +- Integration tests using `WebApplicationFactory` |
| 18 | + |
| 19 | +## Architecture |
| 20 | + |
| 21 | +``` |
| 22 | +Client |
| 23 | + | |
| 24 | + |-- POST /auth/token |
| 25 | + | | |
| 26 | + | +-- signed JWT |
| 27 | + | |
| 28 | + +-- Authorization: Bearer <token> |
| 29 | + | |
| 30 | + v |
| 31 | + Protected Notes API |
| 32 | + | |
| 33 | + |-- ownership check |
| 34 | + +-- per-user rate limit |
| 35 | +``` |
| 36 | + |
| 37 | +## File structure |
| 38 | + |
| 39 | +``` |
| 40 | +aspnet-core/ |
| 41 | ++-- api-security-in-practice/ |
| 42 | + |-- ApiSecurityMinimal.slnx |
| 43 | + |-- README.md |
| 44 | + |-- src/ |
| 45 | + | +-- ApiSecurityMinimal/ |
| 46 | + | |-- ApiSecurityMinimal.csproj |
| 47 | + | |-- Program.cs |
| 48 | + | |-- Models.cs |
| 49 | + | |-- DemoStore.cs |
| 50 | + | |-- JwtTokenService.cs |
| 51 | + | +-- appsettings.json |
| 52 | + +-- tests/ |
| 53 | + +-- ApiSecurityMinimal.Tests/ |
| 54 | + |-- ApiSecurityMinimal.Tests.csproj |
| 55 | + +-- ApiSecurityTests.cs |
| 56 | +``` |
| 57 | + |
| 58 | +## Prerequisites |
| 59 | + |
| 60 | +- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) |
| 61 | + |
| 62 | +No database, Docker, or external service is required. |
| 63 | + |
| 64 | +## Demo credentials |
| 65 | + |
| 66 | +| Field | Value | |
| 67 | +| --- | --- | |
| 68 | +| Email | `demo@example.com` | |
| 69 | +| Password | `DemoPass123!` | |
| 70 | + |
| 71 | +These are development-only credentials. Production systems must use ASP.NET Core Identity or a proper password hasher. |
| 72 | + |
| 73 | +## Setup |
| 74 | + |
| 75 | +Set a JWT signing key using a method that does not commit the key to source control. |
| 76 | + |
| 77 | +### Environment variable (recommended for CI / quick testing) |
| 78 | + |
| 79 | +```powershell |
| 80 | +$env:Jwt__Key = "<development-key-at-least-32-bytes>" |
| 81 | +``` |
| 82 | + |
| 83 | +### User secrets (recommended for local development) |
| 84 | + |
| 85 | +```powershell |
| 86 | +dotnet user-secrets init --project src/ApiSecurityMinimal |
| 87 | +dotnet user-secrets set "Jwt:Key" "<development-key-at-least-32-bytes>" --project src/ApiSecurityMinimal |
| 88 | +``` |
| 89 | + |
| 90 | +## Run |
| 91 | + |
| 92 | +```powershell |
| 93 | +cd aspnet-core\api-security-in-practice |
| 94 | +
|
| 95 | +dotnet restore |
| 96 | +dotnet build --configuration Release --no-restore |
| 97 | +
|
| 98 | +# Run tests |
| 99 | +dotnet test --configuration Release --no-build |
| 100 | +
|
| 101 | +# Run the API |
| 102 | +$env:Jwt__Key = "<development-key-at-least-32-bytes>" |
| 103 | +dotnet run --project src\ApiSecurityMinimal --configuration Release --no-build |
| 104 | +``` |
| 105 | + |
| 106 | +### Manual API test |
| 107 | + |
| 108 | +```powershell |
| 109 | +# Login |
| 110 | +curl.exe -X POST "http://localhost:<port>/auth/token" ` |
| 111 | + -H "Content-Type: application/json" ` |
| 112 | + -d "{\"email\":\"demo@example.com\",\"password\":\"DemoPass123!\"}" |
| 113 | +
|
| 114 | +# Use the returned token for protected endpoints |
| 115 | +curl.exe "http://localhost:<port>/notes" ` |
| 116 | + -H "Authorization: Bearer <token>" |
| 117 | +``` |
| 118 | + |
| 119 | +The actual local port is shown by `dotnet run` and varies per run. |
| 120 | + |
| 121 | +## Verify |
| 122 | + |
| 123 | +| Request | Expected result | |
| 124 | +| --- | --- | |
| 125 | +| `GET /health` (anonymous) | 200 `{"status":"healthy"}` | |
| 126 | +| `POST /auth/token` with valid credentials | 200 + JWT | |
| 127 | +| `POST /auth/token` with invalid credentials | 401 | |
| 128 | +| `GET /notes` without token | 401 | |
| 129 | +| `GET /notes` with valid token | 200 + notes (initially empty) | |
| 130 | +| `DELETE /notes/{id}` owned by another user | 404 | |
| 131 | +| 6th login attempt within a minute | 429 + `Retry-After` | |
| 132 | + |
| 133 | +## Important boundary |
| 134 | + |
| 135 | +This is an **educational JWT-only sample**, not a complete identity system. It intentionally omits: |
| 136 | + |
| 137 | +- Cookie authentication |
| 138 | +- CSRF tokens and antiforgery middleware |
| 139 | +- CORS configuration |
| 140 | +- HSTS and browser security headers |
| 141 | +- ASP.NET Core Identity |
| 142 | +- Password hashing (plain-text demo password) |
| 143 | +- Refresh tokens and token revocation |
| 144 | +- Persistent database |
| 145 | +- Distributed rate limiting |
| 146 | +- OAuth 2.0 / OpenID Connect |
| 147 | +- Production deployment |
| 148 | + |
| 149 | +The full tutorial remains the authoritative source for all of the above. |
| 150 | + |
| 151 | +## Version note |
| 152 | + |
| 153 | +The tutorial is written around **ASP.NET Core 8**. The companion sample targets **.NET 10** while using the same core authentication, authorization, and rate-limiting concepts. |
| 154 | + |
| 155 | +## Security disclosures |
| 156 | + |
| 157 | +- The demo password is intentionally simple and local-only |
| 158 | +- Production systems should use ASP.NET Core Identity or an external identity provider |
| 159 | +- Production signing keys belong in a secret manager |
| 160 | +- Access tokens should be short-lived |
| 161 | +- Refresh-token rotation and revocation are not implemented |
| 162 | +- Cookie authentication and CSRF protection are intentionally excluded |
| 163 | +- CORS and browser security headers are intentionally excluded |
| 164 | +- Rate limiting is in-process and not distributed across instances |
| 165 | +- The in-memory store resets on restart |
| 166 | + |
| 167 | +## Verification details |
| 168 | + |
| 169 | +| Item | Value | |
| 170 | +| --- | --- | |
| 171 | +| Target framework | `net10.0` | |
| 172 | +| `Microsoft.AspNetCore.Authentication.JwtBearer` | 10.0.10 | |
| 173 | +| `Microsoft.AspNetCore.Mvc.Testing` | 10.0.10 | |
| 174 | +| `xunit.v3` | 3.2.2 | |
| 175 | +| `xunit.runner.visualstudio` | 3.1.5 | |
| 176 | +| `Microsoft.NET.Test.Sdk` | 17.14.1 | |
| 177 | +| External services required | None | |
| 178 | +| Containers required | None | |
| 179 | +| API keys required | None | |
| 180 | +| Runtime secret required | Development JWT signing key | |
| 181 | +| Last reviewed | July 28, 2026 | |
| 182 | +| Release build | Verified | |
| 183 | +| Tests | Verified (6/6 passing) | |
| 184 | + |
| 185 | +## License |
| 186 | + |
| 187 | +Sample code in this folder is available under the [MIT License](../LICENSE). |
0 commit comments