Cookies & sessions
Modgud uses cookie-based authentication with ASP.NET Core Identity. No JWTs in the browser — all session state lives on the server.
How it works
ASP.NET Core Identity issues an encrypted auth cookie on login. The cookie holds the ClaimsPrincipal (user id, roles, security stamp) encrypted with Data Protection. On every request, the cookie middleware decrypts it and populates HttpContext.User.
Cookie configuration
Configured in Program.cs:
.AddCookie(IdentityConstants.ApplicationScheme, options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.Name = "Modgud.Auth";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = ctx => { ctx.Response.StatusCode = 401; ... };
options.Events.OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = 403; ... };
})| Property | Value | Purpose |
|---|---|---|
HttpOnly | true | XSS mitigation — JS can't read the cookie |
SecurePolicy | SameAsRequest | Cookie is marked Secure when the request itself is HTTPS (reflecting the real scheme behind a reverse proxy), so it's HTTPS-only in prod while still working over the plain-HTTP Vite dev proxy |
SameSite | Lax | Required for cross-site OIDC redirect-back navigations |
ExpireTimeSpan | 30 days | Max lifetime of persistent cookies |
SlidingExpiration | true | Refresh on active use |
Cookies in detail
| Cookie | SameSite | Purpose | Lifetime |
|---|---|---|---|
Modgud.Auth | Lax | Main session (app cookie) | 30 days (or session-only with RememberMe=false) |
Modgud.2FA | Strict | UserId holder between password step and 2FA step | 5 min |
Modgud.2FA.Remember | Strict | "Remember this browser, skip 2FA" — Identity.TwoFactorRememberMe scheme | Identity default (30 days) |
Modgud.External | Lax | OIDC callback holder | 10 min |
Modgud.Session | Strict | Passkey attestation options only (ASP.NET session) | 5 min idle |
The main Modgud.Auth cookie is Lax (not Strict) — Strict would drop the cookie on the top-level GET redirect-back that OIDC clients use, breaking SSO. Modgud relies on CsrfDefenseMiddleware + the distinct cookie-scheme-per-step design (the 2FA / Session cookies are Strict) for CSRF protection rather than blanket-Strict on the main session.
API response handling
For API calls, the cookie events return status codes instead of redirects:
- Unauthenticated →
401(no redirect to login page) - Forbidden →
403(no redirect to access-denied) - OAuth flow
/connect/authorizeis the exception — it allows redirects so the frontend can drive the login flow
Multi-realm cookies
In modgud the realm boundary is the domain (Host header), not the URL path. Cookies are not path-scoped — they live under the realm domain. A login on acme.example.com isn't sent to a request on finance.example.com, and vice versa.
That means the browser itself won't attach one realm's cookie to a request against another realm's domain — no path acrobatics, no Cookie.Path to set.
Within a single realm, a realm can have multiple apps hosted on subdomains of the realm's primary domain (e.g. app1.acme.example.com and app2.acme.example.com alongside acme.example.com itself). When the request host is the primary domain or one of those app subdomains, the auth cookie's Domain is widened to the primary domain, so one login is shared across all of that realm's apps — enabling single sign-on between them. A realm reached through any other, unrelated domain still gets a host-only cookie scoped to exactly that host.
Single-domain dev setup
In dev, everything runs under localhost:4300 (Vite proxy). Only the system realm exists there (single-tenant fallback in RealmCache). To test multi-realm in dev, use hosts-file entries or *.localtest.me style domains.
Session tracking
In parallel with the auth cookie, modgud maintains a UserSession Marten document per active login. This enables session-management features (list sessions, revoke individually, log out everywhere) that a cookie alone can't provide.
Session cookie
The Modgud.Session cookie (HttpOnly, Secure in prod) correlates the browser with the UserSession document. On logout, the document is deleted and the cookie is cleared.
UserSession document
| Field | Source | Purpose |
|---|---|---|
UserId | Auth system | Link |
SessionId | Random GUID | Correlation with cookie |
IpAddress | HttpContext.Connection.RemoteIpAddress (proxy-aware via ForwardedHeaders) | Audit |
Browser, BrowserVersion | UAParser | UI display |
OperatingSystem, OsVersion | UAParser | UI display |
DeviceType | UAParser | Desktop/Mobile/Tablet |
CreatedAt, LastActiveAt, ExpiresAt | UTC | TTL + UI |
SessionTracker updates LastActiveAt on every authenticated request, throttled (e.g. at most once per minute per session).
Self-service endpoints
GET /api/auth/sessions
DELETE /api/auth/sessions/{id}
DELETE /api/auth/sessions # all except currentAdmin variants
GET /api/admin/users/{id}/sessions
DELETE /api/admin/users/{id}/sessions # force logoutForced logout via security stamp
ASP.NET Core Identity has a SecurityStamp mechanism: on security-relevant events (password change, 2FA toggle) the stamp is invalidated; on the next cookie validation the cookie is rejected and the user is logged out.
Modgud uses that plus the UserSession documents: "Log out everywhere" clears all UserSessions + invalidates the security stamp → all of the user's cookies are rejected on the next validation.
Security summary
| Concern | Mitigation |
|---|---|
| XSS token theft | HttpOnly |
| Man-in-the-middle | Secure (prod) |
| CSRF | SameSite=Lax on the main cookie + Strict on 2FA/Session step cookies + CsrfDefenseMiddleware on mutating endpoints |
| Cross-realm leakage | Realm domain → own cookie domain |
| Forced logout | Security stamp + delete UserSession document |
| Account lockout | 5 failed logins → 1 min lockout (DoS limit) |