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; can be switched off per realm or AppSession or realm browser-session policy (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)Realm browser-session policy
Magic LinkEmail with single-use token; can also be sent by an adminRealm browser-session policy
OIDC/SAML ExternalFederated login via an upstream IdPRealm browser-session policy

See Login flows for details.

Sign-in level ​

How strong a sign-in must be is a realm and App setting, decided by the app being accessed — see Sign-in levels. In short:

LevelReached by
singleOne factor: password, e-mail code, magic link
multiTwo different factors, or a passkey / federated MFA on its own

Each App has a minimum level, the sign-in methods it offers and a setup grace period for users without a second factor; the realm holds the defaults. A session that is below the target App's level is stepped up with only the missing factor. There is no deployment-wide sign-in level; a realm that never saved its policy runs with floor single and administration multi.

Cookies ​

CookiePurposeSameSiteLifetime
Modgud.AuthMain session (HttpOnly)LaxSession or realm browser-session policy
Modgud.2FAUserId between password step and 2FA stepStrict5 min
Modgud.ExternalOIDC callback holderLax10 min
Modgud.Passkey.Challenge / Modgud.Passkey.EnrollId of the server-side passkey login / registration ceremonyStrict5 min

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)

There are no recovery codes. A user who loses a factor falls back to another enrolled method — after a password the login page also offers the e-mail code (see Sign-in levels) — or the operator resets the factor with the recovery CLI.

Passkeys are bound to their relying-party ID

A passkey is registered against a WebAuthn relying-party ID. On the web, Modgud uses the realm's PrimaryDomain as that ID: such a passkey 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 those passkeys (affected users must re-register). A native app can carry its own RP ID (ADR 0009); its passkeys are bound to that ID instead. See Realms — primary domain.

External login (OIDC and SAML) ​

Users can sign in through Microsoft Entra ID and standards-compatible OIDC or SAML providers. Providers are configured independently per realm.

  1. Admin creates an OIDC or SAML LoginProvider.
  2. The login page shows a button for every enabled external provider.
  3. OIDC uses Authorization Code + PKCE. SAML uses an SP-initiated AuthnRequest and a correlated ACS response.
  4. After protocol validation, 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. The realm's browser-session policy determines the login-cookie lifetime.

Modgud consumes SAML only as a Service Provider and accepts only SP-initiated, correlated responses. IdP-initiated SSO, SAML Single Logout and Artifact Binding are outside the v1 surface. See Login providers and SAML federation.

Account lifecycle ​

How does a user enter the system?Mechanism
Self-registrationRegistration form (when enabled for the realm)
External loginOIDC/SAML 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 an administrator. Wrong-password floods no longer lock the account itself: failures are throttled per device and per untrusted pool so the owner's own browsers keep working (see Rate limits → Password login)
  • Soft-deleted — IsDeleted = 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.