Finishes functional design for unit 0 (back-end changes before front-end work)

This commit is contained in:
2026-06-18 21:40:45 +02:00
parent 9e49489f7e
commit 53a307cdbd
14 changed files with 1065 additions and 4 deletions
@@ -0,0 +1,53 @@
# NFR Requirements — Unit 0: Backend Prerequisites
## NFR-U0-01: Security — httpOnly Cookie (SECURITY-12)
- Refresh token MUST be stored in an httpOnly, Secure (request.IsHttps), SameSite=Strict cookie
- Access token MUST NOT be returned in persistent storage; only in response body and in-memory on client
- See `business-rules.md` BR-U0-02, BR-U0-03 for detailed cookie specification
## NFR-U0-02: Security — CORS (SECURITY-08)
- CORS MUST use explicit origin allowlist from configuration — no wildcard origins
- `AllowCredentials()` MUST be set to allow the refresh token cookie to be sent cross-origin
- CORS middleware MUST be registered before `UseAuthentication` in the pipeline
- An empty `AllowedOrigins` array in production configuration is a safe default (no SPA access)
## NFR-U0-03: Security — Rate Limiting (SECURITY-11)
- The `POST /api/v1/auth/login` and `POST /api/v1/auth/refresh` endpoints MUST have rate limiting
- **Implementation**: ASP.NET Core built-in `RateLimiter` middleware (available in .NET 7+)
- **Policy**: Fixed window — max **5 requests per 1 minute per IP address** on login endpoint
- **Policy**: Sliding window — max **20 requests per 1 minute per IP address** on refresh endpoint
- Exceeded rate limit returns `429 Too Many Requests`
- Rate limit headers (`Retry-After`) MUST be included in 429 responses
- Configuration stored in `appsettings.json → RateLimiting` section
## NFR-U0-04: Reliability — Refresh Token Lifetime
- Refresh token lifetime: **7 days** (unchanged from current default)
- After expiry, the user must re-authenticate via the login page
- Refresh token rotation is already implemented — each refresh issues a new token
## NFR-U0-05: Error Response Format (SECURITY-09, SECURITY-15)
- ALL authentication error responses MUST use **RFC 9457 ProblemDetails** format:
```json
{
"type": "https://tools.ietf.org/html/rfc7235#section-3.1",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid credentials."
}
```
- Error messages MUST be generic — do NOT reveal whether email or password was wrong
- Error messages MUST NOT expose internal details (stack traces, exception types, DB details)
- `asp-problem-details` behavior is already partially handled by `GlobalExceptionHandler`; auth-specific errors may need explicit `ProblemDetails` returns in `AuthController`
## NFR-U0-06: Maintainability
- Rate limiting configuration (window size, request limits) stored in `appsettings.json` — configurable without code changes
- CORS origins stored in `appsettings.json` — configurable per environment
- No magic strings for cookie name or policy name — use constants
## NFR-U0-07: Test Coverage
- Unit tests for `AuthController` must be updated to cover:
- Cookie is set on successful login/refresh
- Cookie is cleared on revoke
- 401 returned when cookie is missing on refresh
- Rate limit behavior (mock rate limiter)
- Existing `AuthService` unit tests remain valid (service signature unchanged)
@@ -0,0 +1,105 @@
# Tech Stack Decisions — Unit 0: Backend Prerequisites
## Existing Stack (no changes)
All existing technology choices are retained:
- **.NET 10 / ASP.NET Core 10** — Web API framework
- **Entity Framework Core 10** — ORM + migrations
- **ASP.NET Core Identity** — User/role management, password hashing
- **SQL Server** — Primary database
---
## New/Updated: Rate Limiting
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Rate limiter | ASP.NET Core built-in `RateLimiter` (Microsoft.AspNetCore.RateLimiting) | No extra NuGet package needed — available in .NET 7+; production-ready |
| Login policy | Fixed window (5 req / 1 min / IP) | Predictable; blocks brute-force login attempts |
| Refresh policy | Sliding window (20 req / 1 min / IP) | More lenient for token rotation; prevents abuse |
| Configuration | `appsettings.json → RateLimiting` | Configurable without code recompile |
---
## New/Updated: CORS
| Decision | Choice | Rationale |
|----------|--------|-----------|
| CORS implementation | ASP.NET Core built-in CORS middleware | No extra NuGet package; part of framework |
| Origins configuration | `appsettings.json → Cors:AllowedOrigins[]` | Environment-specific; follows dotnet-appsettings pattern |
| Credential support | `AllowCredentials()` | Required for httpOnly cookie to be sent cross-origin |
| Methods | `AllowAnyMethod()` | Avoids future CORS issues when new endpoints are added |
| Headers | `AllowAnyHeader()` | Standard approach; avoids pre-flight failures for custom headers |
---
## New/Updated: httpOnly Cookie
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Cookie implementation | ASP.NET Core `Response.Cookies.Append()` | Built-in, no extra library |
| Token read | `Request.Cookies["refreshToken"]` | Standard ASP.NET Core cookie reading |
| Secure flag | `request.IsHttps` | Adapts to environment; safe in production, usable in local HTTP dev |
| SameSite | `Strict` | Maximum CSRF protection |
---
## New/Updated: Error Responses
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Error format | RFC 9457 ProblemDetails | .NET standard; consistent with ASP.NET Core defaults; interoperable |
| Implementation | `Microsoft.AspNetCore.Mvc.ProblemDetails` (built-in) | No extra NuGet package needed |
| Global handler | Existing `GlobalExceptionHandler` extended | Avoids duplication; centralises error formatting |
---
## appsettings.json Additions
Following the dotnet-appsettings skill pattern:
```json
// appsettings.json (production defaults — no real values)
{
"Cors": {
"AllowedOrigins": []
},
"RateLimiting": {
"Login": {
"PermitLimit": 5,
"WindowSeconds": 60
},
"Refresh": {
"PermitLimit": 20,
"WindowSeconds": 60
}
}
}
```
```json
// appsettings.Development.json (complete reference for developers)
{
"Cors": {
"AllowedOrigins": ["http://localhost:5173"]
},
"RateLimiting": {
"Login": {
"PermitLimit": 5,
"WindowSeconds": 60
},
"Refresh": {
"PermitLimit": 20,
"WindowSeconds": 60
}
}
}
```
```json
// appsettings.local.json (developer override — gitignored)
{
"Cors": {
"AllowedOrigins": ["http://localhost:5173"]
}
}
```