Skip to content

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)

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

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:

  1. Read the techstrat_session cookie
  2. Check the SessionCache (in-memory ConcurrentDictionary with 5-minute TTL)
  3. If not cached, load the session from the database (eager-loads the associated User)
  4. Verify the session is not expired
  5. Build a ClaimsPrincipal with 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=Strict cookie 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 matches
  • SuccessRehashNeeded — 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