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.

Native app grants

Native and headless clients (mobile apps, CLIs) can sign in directly against /connect/token without ever holding a browser cookie, using one of three cookieless grants: email OTP (urn:cocoar:otp), magic link (urn:cocoar:magic), or passkey (urn:cocoar:passkey). A signed-in native client can also list and revoke its own passkeys via GET/DELETE /connect/passkey. These grants are opt-in per OAuth client and disabled by default. See Native app integration for the full flows.

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.

Passkeys are bound to the realm's primary domain

A passkey is registered against a WebAuthn relying-party ID, and Modgud uses the realm's PrimaryDomain as that ID. A passkey therefore only works when the user reaches the realm on its primary domain — not via a secondary domain in the realm's Domains list — and changing the realm's PrimaryDomain invalidates every existing passkey (affected users must re-register). See Realms — primary domain.

External login (OIDC IdPs and SAML)

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.

Modgud also supports SAML 2.0 as an external login provider type (LoginProvider of Type = Saml), for IdPs that only speak SAML. It follows the same JIT-create-on-first-login shape as OIDC. See SAML federation for setup.

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)

Self-registration can also be gated by an invite code: an Application can require a valid, unused, single-use code before an unknown email is allowed to self-register, while already-known users keep signing in normally. See Applications for how to turn this on per Application.

Released under the Apache-2.0 License.