Skip to content

Authentication

Modgud has two orthogonal authentication axes:

  1. First-party login — the user signs in to modgud itself (admin UI, profile, setup). Cookie-based, no token in the browser.
  2. OAuth/OIDC server — external apps let users sign in via modgud. Authorization Code + PKCE, classic.

Both share the same login methods under the hood.

First-party login

Implemented in the Authentication slice (Modgud.Authentication). Endpoints mounted under /api/account/....

Login methods

MethodWhenCookie lifetime
PasswordDefault, allowed at AuthLevel 0/1Session or 30 days (RememberMe)
TOTPSecond factor after passwordInherits from the password step
Email OTPSecond factor — or as an alternative loginInherits from the password step
Passkey (FIDO2)Second factor — or as a sole login (passwordless)Always 30 days (persistent)
Magic LinkEmail with single-use token; can also be sent by an adminAlways 30 days
OIDC ExternalFederated login via Entra ID, Google, ...30 days

See Login flows for details.

Authentication level

Configured globally via IAuthSettings.AuthenticationMinimumLevel:

LevelEffect
0 = NonePassword-only allowed — no enforcement
1 = SecureLogin (default)User must have 2FA or a passwordless method
2 = PasswordlessPassword login disabled — only Magic Link + Passkey

At level >= 1 the TwoFactorEnforcementMiddleware runs and blocks authenticated requests from users without 2FA (with a grace period).

Cookies

CookiePurposeSameSiteLifetime
Modgud.AuthMain session (HttpOnly)LaxSession or 30 days
Modgud.2FAUserId between password step and 2FA stepStrict5 min
Modgud.ExternalOIDC callback holderLax10 min
Modgud.SessionOnly for passkey attestation optionsStrict5 min idle

SameSite=Lax on the main session cookie is required so that OIDC redirect-back navigations carry the cookie (top-level GET → cookie sent). Cross-site POSTs are still blocked by SameSite=Lax, plus the CsrfDefenseMiddleware rejects state-changing requests whose Sec-Fetch-Site indicates cross-origin.

In production all cookies are Secure. In dev Secure=None so the Vite dev server (http://localhost:4300) can write them.

OAuth 2.0 / OIDC server

Modgud is at the same time a full-fledged OpenID Connect provider for external apps. Implemented via OpenIddict 7 with its own Marten-based stores (no Entity Framework).

Flows

Supported: Authorization Code + PKCE, Client Credentials, Refresh Token.

Not supported: Implicit Flow, ROPC.

See OAuth & OIDC and OAuth implementation for details.

Per-realm isolation

Each realm is its own OIDC provider with its own discovery document at https://<realm-domain>/.well-known/openid-configuration. Tokens from realm A do not work in realm B — the issuer check blocks them.

This is implemented by the RealmIssuerHandler (an OpenIddict pipeline hook): at boot there is a static issuer; the handler overrides it per request with BaseUri (the current realm domain).

Multi-factor authentication

Three independent 2FA methods, freely combinable:

MethodHow it works
TOTPAuthenticator app (Google Authenticator, Authy) — RFC 6238
Email OTPOne-time code by email to the verified address
WebAuthn/PasskeyHardware keys (YubiKey) or platform authenticators (TouchID, Windows Hello)

Plus recovery codes as a last-resort backup.

External login (OIDC IdPs)

Users can sign in via external OIDC providers (Entra ID, Google, Auth0, ...). Configurable per realm.

  1. Admin creates a LoginProvider of Type = Oidc: authority, client ID, client secret, UserUpdateScript
  2. Login page automatically shows buttons for enabled OIDC providers
  3. Click → OIDC Authorization Code + PKCE → IdP login
  4. On callback: ExternalLoginProcessor runs
    • Looks up ExternalIdentityLink (issuer + subject) → existing user or JIT-create
    • UserUpdateScript (Jint) maps claims to user fields
  5. If the user has 2FA enabled, the normal 2FA flow runs afterwards
  6. Login cookie is set (always 30 days)

See Login providers (OIDC) for details.

Account lifecycle

How does a user enter the system?Mechanism
Self-registrationRegistration form (when enabled for the realm)
External loginOIDC IdP → JIT-create on first login
Admin-createdAdmin creates the user via the UI
SetupFirst-time setup — the first user becomes system admin

Lifecycle states:

  • Active — normal state
  • Locked — by account lockout (5 failed logins → 1 min)
  • Soft-deletedIsDeleted = true, all data preserved, reactivatable
  • GDPR-erased — stream archived, PII masked, irreversible (Article 17)

Detailed slice-internal walkthrough lives in the repo-only Authentication slice blueprint notes.

Released under the Apache-2.0 License.