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
| Method | Path | Description |
|---|---|---|
POST | /api/account/login | Login with username + password |
POST | /api/account/logout | Logout (cookie removed, session invalidated) |
POST | /api/account/register | Self-registration (when enabled per realm) |
POST | /api/account/forgot-password | Request a password-reset link |
POST | /api/account/reset-password | Reset password with a token |
Email verification
| Method | Path | Description |
|---|---|---|
POST | /api/account/email/send-verification | Send (or resend) a verification email for the signed-in user's address |
POST | /api/account/email/verify | Anonymous — verify with the token from the email |
Current user & profile
| Method | Path | Description |
|---|---|---|
GET | /api/account/me | Current user info (incl. effective permissions, realm slug) |
GET | /api/account/profile | Detailed profile |
PUT | /api/account/profile | Edit profile (creates a UserChangeRequest for non-email fields) |
POST | /api/account/change-password | Change password |
GET | /api/account/external-links | Linked OIDC identities for the signed-in user |
DELETE | /api/account/external-links/{linkId} | Remove a link |
Two-factor authentication
Status & TOTP
| Method | Path | Description |
|---|---|---|
GET | /api/account/mfa/status | 2FA status (enabled, methods, recoveryCodesRemaining) |
POST | /api/account/mfa/setup | Generate TOTP authenticator key + QR URI |
POST | /api/account/mfa/enable | Enable 2FA with TOTP code |
POST | /api/account/mfa/disable | Disable 2FA |
POST | /api/account/mfa/recovery-codes | Regenerate recovery codes |
POST | /api/account/mfa/login | Login step 2 with TOTP code |
POST | /api/account/mfa/recovery-login | Login with recovery code |
Email OTP
| Method | Path | Description |
|---|---|---|
GET | /api/account/email-otp/status | Email-OTP enrolment status |
POST | /api/account/email-otp/login/request | Request email OTP for login |
POST | /api/account/email-otp/login | Login with email OTP |
Passkey / FIDO2 / WebAuthn
| Method | Path | Description |
|---|---|---|
POST | /api/account/passkey/register/options | Registration options |
POST | /api/account/passkey/register/complete | Complete registration |
POST | /api/account/passkey/login/options | Login options (with or without userName for passwordless) |
POST | /api/account/passkey/login/complete | Complete login |
GET | /api/account/passkey/credentials | List own passkeys |
DELETE | /api/account/passkey/credentials/{id} | Delete a passkey |
PATCH | /api/account/passkey/credentials/{id} | Change a passkey label |
Magic link
| Method | Path | Description |
|---|---|---|
POST | /api/account/magic-link/request | Request a magic link (self-service, only when enabled) |
GET | /api/account/magic-link/login?token=…&user=… | Magic-link login |
External login (OIDC)
| Method | Path | Description |
|---|---|---|
GET | /api/account/external-logins | List of active LoginProviders (no secrets) |
GET | /api/account/external-login/{loginProviderId}/start?returnUrl=/ | Start OIDC flow |
GET | /api/account/external-login/finish | OIDC 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 → returnUrlSessions
| Method | Path | Description |
|---|---|---|
GET | /api/account/sessions | Active sessions |
DELETE | /api/account/sessions/{id} | Revoke a session |
DELETE | /api/account/sessions | Revoke 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:
| Method | Path | Description |
|---|---|---|
GET | /api/auth/export-data | Data export (Article 20) — ZIP |
GET | /api/auth/deletion-status | Status of the delete workflow |
POST | /api/auth/delete-account | Request account deletion (token email goes out) |
POST | /api/auth/confirm-deletion | Confirm with token → archive stream + mask PII |
POST | /api/auth/cancel-deletion | Cancel 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:
| Method | Path | Description |
|---|---|---|
POST | /api/account/bootstrap-admin | Consume a single-use invite token + set password. Body: { "Token": "<plaintext>", "Password": "<new>" }. On success: user is created, atomically added to the Administratoren 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 CLIPOST /api/admin/realmswith anInitialAdminpayload — see Realm APIPOST /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) nullfields 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
| Status | Meaning |
|---|---|
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 |
401 | Not authenticated or wrong credentials |
403 | Authenticated but no permission, or passwordless-only realm |
429 | Rate limit (Email OTP, Magic Link, bootstrap-admin) |