Authentication & Security¶
The Tech Strategy Tool uses cookie-based session authentication with role-based access control. This page covers the authentication flow, session management, CSRF protection, and security hardening measures.
Authentication Flow¶
sequenceDiagram
participant Browser
participant API as API Host
participant DB as PostgreSQL
Browser->>API: POST /api/auth/login {username, password}
API->>DB: Look up user by username
API->>API: Verify password hash
API->>DB: Create SessionRecord (7-day expiry)
API-->>Browser: Set-Cookie: techstrat_session=... (HttpOnly, Secure, SameSite=Strict)
Note over Browser,API: Subsequent requests include cookie automatically
Browser->>API: GET /api/teams (cookie attached)
API->>API: SessionAuthenticationHandler reads cookie
API->>API: Load session (cache or DB)
API->>API: Build ClaimsPrincipal
API-->>Browser: 200 OK (team data)
Why Cookie-Based Sessions?¶
The choice of cookie-based sessions over JWTs is deliberate:
Immediate role enforcement. When an administrator changes a user's role or deletes their account, it must take effect quickly. With server-validated sessions, every request checks the user's current state against the database (or a short-lived cache). With JWTs, the role is encoded in the token and the user continues to operate with the old role until the token expires.
Simplicity for a single-instance tool. There is no need for stateless authentication across multiple services. A single server validates a single session store. JWTs solve a distributed trust problem that does not exist here.
SSE connection authentication. SSE connections are long-lived HTTP connections. Cookie-based auth works naturally — the browser sends the cookie on connection open. JWTs in SSE would require either URL-embedded tokens (security risk) or custom header mechanisms that the EventSource API does not support natively.
Session Management¶
Cookie properties¶
| Property | Value | Purpose |
|---|---|---|
| Name | techstrat_session |
Session identifier |
| HttpOnly | true |
Prevents JavaScript access (XSS mitigation) |
| Secure | true |
Cookie only sent over HTTPS |
| SameSite | Strict |
Cookie not sent on cross-origin requests (CSRF mitigation) |
| Expiry | 7 days | Session lifetime |
Session authentication handler¶
SessionAuthenticationHandler is a custom ASP.NET Core authentication handler registered as the default scheme. On each request:
- Read the
techstrat_sessioncookie - Check the
SessionCache(in-memoryConcurrentDictionarywith 5-minute TTL) - If not cached, load the session from the database (eager-loads the associated
User) - Verify the session is not expired
- Build a
ClaimsPrincipalwith claims:NameIdentifier,Name,Role,SessionId
Session caching
The SessionCache is a singleton with a ConcurrentDictionary<string, CachedSession>. Each entry has a 5-minute TTL. This avoids hitting the database on every request — important for a real-time collaborative tool where SSE notifications trigger frequent card re-fetches. The cache is evicted explicitly on logout (Evict) and user deletion (EvictByUserId).
Cache TTL trade-off
The 5-minute TTL means a role change could take up to 5 minutes to take effect for an active user. The explicit eviction methods handle the critical paths (logout, user deletion) where immediate effect is needed. The handler uses IServiceScopeFactory to create its own DI scope, avoiding the "captive dependency" anti-pattern (the handler is singleton-scoped, but repositories are scoped).
Session cleanup¶
SessionCleanupService is a hosted service that runs every hour and deletes expired sessions from the database via ISessionRepository.DeleteExpiredAsync().
Role-Based Access Control¶
Three roles with hierarchical permissions:
| Role | Capabilities |
|---|---|
| Viewer | Read-only access to strategy content |
| Editor | All viewer capabilities + create and edit strategy content |
| Admin | All editor capabilities + manage users, teams, event log, restore |
Authorization policies¶
Three policies are defined in RolePolicies:
| Policy | Allowed Roles |
|---|---|
ViewerOrAbove |
viewer, editor, admin |
EditorOrAbove |
editor, admin |
AdminOnly |
admin |
These are applied to endpoint groups and individual endpoints. Some endpoints have additional role checks within the handler (e.g., team-level events and restore_history require admin role even though the events endpoint only requires EditorOrAbove).
CSRF Protection¶
CsrfMiddleware requires an X-CSRF-Token: 1 header on all mutating requests (POST, PUT, PATCH, DELETE) to /api/* paths. This is a simple CSRF mitigation:
- Browsers cannot set custom headers on cross-origin requests without CORS preflight
- The
SameSite=Strictcookie policy provides a second layer of defense - Login (
POST /api/auth/login) is exempt since no session exists yet
If the header is missing, the middleware returns 403 Forbidden.
Client implementation
Both the Strategy Site and Admin Site API clients include X-CSRF-Token: 1 on all mutating requests automatically.
Login Rate Limiting¶
LoginRateLimiter protects against brute-force password attacks:
- Tracks failed login attempts per username using
ConcurrentDictionary - After 5 failed attempts within 15 minutes, the account is locked out
- Successful login clears the failure counter
- The limiter is in-memory only — counters reset on application restart
In-memory limitation
Since the rate limiter is in-memory, it resets when the application restarts. For production deployments with higher security requirements, consider a persistent rate limiting solution.
Password Hashing¶
PasswordHasher wraps ASP.NET Core's PasswordHasher<string>, which uses PBKDF2 by default. On verification:
Success— password matchesSuccessRehashNeeded— password matches but was hashed with an older algorithm; treated as success (no rehash is performed)Failed— password does not match
Security Summary¶
| Measure | Implementation |
|---|---|
| Authentication | Cookie-based sessions with 7-day expiry |
| Session storage | PostgreSQL with periodic cleanup |
| Password hashing | ASP.NET Core Identity PBKDF2 |
| CSRF protection | Custom header requirement + SameSite=Strict cookies |
| XSS prevention | HttpOnly cookies, HTML escaping in HighlightRenderer |
| Rate limiting | Per-username lockout after 5 failures in 15 minutes |
| Input validation | Two-layer: HTTP layer validates envelope structure and admin/auth fields (Roles.All, UserFieldLimits); domain processor validates business rules and FieldLimits |
| Authorization | Role-based policies on all endpoints |
Key Files¶
| File | Purpose |
|---|---|
src/TechStrat.Api/Auth/SessionAuthenticationHandler.cs |
Cookie-based session authentication |
src/TechStrat.Api/Auth/SessionCache.cs |
In-memory session cache (5-min TTL) |
src/TechStrat.Api/Auth/CsrfMiddleware.cs |
CSRF header validation |
src/TechStrat.Api/Auth/LoginRateLimiter.cs |
Brute-force protection |
src/TechStrat.Api/Auth/PasswordHasher.cs |
Password hash/verify wrapper |
src/TechStrat.Api/Auth/RolePolicies.cs |
Authorization policy definitions (uses Roles.* constants) |
src/TechStrat.Api/Auth/SessionCleanupService.cs |
Expired session cleanup |
src/TechStrat.Core/Auth/Roles.cs |
Role constants (Viewer, Editor, Admin) and All set |
src/TechStrat.Core/Auth/UserFieldLimits.cs |
Username/password length constraints |