Skip to content

Auth Endpoints

Endpoints under /api/account/... (and a handful of identity-lifecycle operations under /api/auth/...). The current realm is resolved via the Host header — no realm path prefixes.

Full endpoint source in src/dotnet/Modgud.Authentication/Api/Account/ and src/dotnet/Modgud.Authentication/Api/ExternalAuth/.

Public authentication

MethodPathDescription
POST/api/account/loginLogin with username + password
POST/api/account/logoutLogout (cookie removed, session invalidated)
GET/api/account/self-registration-infoAnonymous — public self-registration config the SPA reads before mounting /register
POST/api/account/registerSelf-registration (when enabled per realm)
POST/api/account/register/verify-emailAnonymous — consume the registration email-verification token
POST/api/account/forgot-passwordRequest a password-reset link
POST/api/account/reset-passwordReset password with a token

Login, magic-link, password-reset, and self-registration all accept and carry forward a validated ?redirect= continuation, so starting one of these flows from an OAuth client's /connect/authorize challenge lands the user back at the client instead of stranding them on the IdP.

Self-registration also supports an invite-code-gated posture: a realm can require a single-use invite code (issued through the Admin API) before an account can be created, instead of open self-registration.

Email verification

MethodPathDescription
POST/api/account/email/send-verificationSend (or resend) a verification email for the signed-in user's address
POST/api/account/email/verifyAnonymous — verify with the token from the email

Current user & profile

MethodPathDescription
GET/api/account/meCurrent user info (incl. effective permissions, realm slug)
PUT/api/account/profile/requestSubmit a profile change request (name/acronym/email)
GET/api/account/profile/requestThe signed-in user's open change request (if any) plus the last terminal (approved/rejected) one
DELETE/api/account/profile/requestCancel the open change request
POST/api/account/profile/request/verify-emailAnonymous — confirm the emailed verification token for a pending email change
POST/api/account/change-passwordChange password
GET/api/account/external-linksLinked OIDC identities for the signed-in user
DELETE/api/account/external-links/{linkId}Disconnect a link — frees the (issuer, subject) so the identity can be re-linked later. Refused (Idp.LastAuthMethod) if it is the only remaining auth method (no password, no passkey, no other link).

Every profile edit goes through a UserChangeRequest — never a direct write. An edit that changes the email address must be verified via the emailed link before it moves to admin approval; edits that only touch name/acronym go straight to admin approval.

Two-factor authentication

Status & TOTP

MethodPathDescription
GET/api/account/mfa/status2FA status (enabled, hasAuthenticator)
POST/api/account/mfa/setupGenerate TOTP authenticator key + QR URI
POST/api/account/mfa/verifyVerify a TOTP code and enable 2FA
POST/api/account/mfa/disableDisable 2FA
POST/api/account/mfa/loginLogin step 2 with TOTP code

There are no recovery codes — losing the authenticator device means falling back to another enrolled method (email OTP, passkey).

Email OTP

MethodPathDescription
GET/api/account/email-otp/statusEmail-OTP enrolment status
POST/api/account/email-otp/login/requestRequest email OTP for login
POST/api/account/email-otp/loginLogin with email OTP

Passkey / FIDO2 / WebAuthn

MethodPathDescription
GET/api/account/passkeyList own passkeys
POST/api/account/passkey/register-optionsRegistration options
POST/api/account/passkey/registerComplete registration
POST/api/account/passkey/login-optionsLogin options (with or without userName for passwordless)
POST/api/account/passkey/loginComplete login
DELETE/api/account/passkey/{id}Delete a passkey
MethodPathDescription
POST/api/account/magic-link/requestRequest a magic link (self-service, only when enabled)
GET/api/account/magic-link/login?token=…&user=…Magic-link login

Native cookieless grants

For native/mobile clients that can't hold a session cookie. These mint a code, or begin a WebAuthn ceremony, over plain JSON, then redeem it as an OAuth grant at /connect/token — see OAuth Endpoints → Supported flows for the urn:cocoar:otp / urn:cocoar:magic / urn:cocoar:passkey grant types. Gated per realm/App behind the NativeGrants flag (default off). The existing /api/account/magic-link/request above doubles as the code-issuance step for the urn:cocoar:magic grant — there is no separate native endpoint for it.

MethodPathDescription
POST/api/account/native/otp/requestEmail a one-time login code (also handles JIT sign-up, under the right posture)
POST/api/account/native/registerExplicit passwordless sign-up — emails a registration code
POST/connect/passkey/beginBegin a usernameless WebAuthn assertion ceremony
GET/connect/passkeyBearer-authenticated — list the signed-in token subject's own passkeys
DELETE/connect/passkey/{id}Bearer-authenticated — revoke one of the token subject's own passkeys

External login (OIDC)

MethodPathDescription
GET/api/account/external-loginsList of active LoginProviders (no secrets)
GET/api/account/external-login/{loginProviderId}/start?returnUrl=/Start OIDC flow
GET/api/account/external-login/finishOIDC callback from the external IdP
GET/api/account/external-logout/{loginProviderId}Single-logout signal back to the external IdP

Login flow

1. Frontend: GET /api/account/external-logins → shows provider buttons
2. User clicks "Login with Acme SSO"
3. Browser: GET /api/account/external-login/{loginProviderId}/start?returnUrl=/
4. Backend: ASP.NET Challenge with the dynamically registered OIDC scheme
5. Browser: 302 → external IdP
6. User authenticates with the IdP
7. IdP: 302 → /api/account/external-login/finish
8. Backend: ExternalLoginProcessor runs (look up user or JIT create,
   run UserUpdateScript, set login cookie)
9. Backend: 302 → returnUrl

Sessions

These live under /api/auth/..., not /api/account/....

MethodPathDescription
GET/api/auth/sessionsActive sessions
DELETE/api/auth/sessions/{id}Revoke a session
DELETE/api/auth/sessionsRevoke all sessions except current ("logout everywhere")

GDPR / privacy

These live under /api/auth/... (separate from the day-to-day account surface) because they're identity-lifecycle operations:

MethodPathDescription
GET/api/auth/export-dataData export (Article 20) — ZIP
GET/api/auth/deletion-statusStatus of the delete workflow
POST/api/auth/delete-accountRequest account deletion (token email goes out)
POST/api/auth/confirm-deletionConfirm with token → archive stream + mask PII
POST/api/auth/cancel-deletionCancel a pending delete request

Bootstrap (first admin in a realm)

There is no anonymous setup wizard. The first admin in any realm is created either through the recovery CLI (filesystem trust) or via a Control-Plane admin issuing an invite through the realm-create API. The single anonymous endpoint is the bootstrap-invite consumer:

MethodPathDescription
POST/api/account/bootstrap-adminConsume a single-use invite token + set password. Body: { "Token": "<plaintext>", "Password": "<new>" }. On success: user is created, atomically added to the Administrators group with realm:admin, and auto-signed-in via cookie.

The token comes from one of:

  • dotnet Modgud.Api.dll recover bootstrap-admin --email <e> (without --password) — see Recovery CLI
  • POST /api/admin/realms with an InitialAdmin payload — see Realm API
  • POST /api/admin/realms/{slug}/resend-bootstrap-invite — re-issue a fresh token for the same recipient

Token properties: SHA-256-hashed in the DB, 7-day TTL, single-use (reuse → 400 BootstrapInvite.TokenUsed). Endpoint is rate-limited under the bootstrap policy (10 attempts per IP per 15 minutes).

Response format conventions

  • All responses use PascalCase JSON (PropertyNamingPolicy = null)
  • null fields are omitted (JsonIgnoreCondition.WhenWritingNull)
  • Enums are serialised as strings
  • Errors as ProblemDetails (application/problem+json)

Anti-enumeration responses

Endpoints that could otherwise reveal account existence (forgot-password, email-OTP login request, magic-link request) deliberately return the same response for "valid email" and "no such user" — and apply an artificial delay on the no-user path so timing analysis is no help either. This applies across the whole password-reset / magic-link / email-OTP family.

Auth status codes

StatusMeaning
200 { authenticated: true, ... }Success (cookie set)
200 { requiresTwoFactor: true, mfaMethods: [...] }Step-2 MFA needed
200 { requiresSecureSetup: true, gracePeriod: true, secureSetupDueAt }User still has to set up 2FA, time remaining
200 { requiresSecureSetup: true, gracePeriod: false }Grace period over, blocking
401Not authenticated or wrong credentials
403Authenticated but no permission, or passwordless-only realm
429Rate limit (Email OTP, Magic Link, bootstrap-admin, native OTP/passkey, …)

The ceilings shown throughout this page (e.g. bootstrap's 10-per-15-minutes) are the defaults — a realm admin can override each one under Realm Settings → Rate Limits.

Released under the Apache-2.0 License.