Realm Settings
Realm Settings are realm-wide configuration owned by the realm admin (not the Control-Plane admin). They live in the tenant database as a singleton document and are managed via Administration → Realm Settings.
Realm structure vs. Realm settings
- Realms (structural metadata: slug, domains, Control-Plane flag, active state) are managed by the Control-Plane admin only.
- Realm Settings (self-registration, DCR policy, branding, …) are managed by the realm admin inside their own realm. The Control-Plane admin reaches their own realm's settings the same way as any other realm admin would — through this page.
Tabs
The page currently has two tabs:
- Self-Registration — public sign-up policy
- Dynamic Client Registration — anonymous OAuth-client registration policy (linked detail page: Dynamic Client Registration)
Per-realm branding is configured on a separate page under Plattform — see Customization → Branding.
Permissions: realm-settings:read / realm-settings:write. The realm-admin role grants both via the realm:admin bypass.
Self-Registration
Public sign-up: visitors can create an account themselves at /register. Opt-in per realm, disabled by default — anonymous probes to /api/account/self-registration-info return the all-defaults shape so disabled realms can't be enumerated.
Enabling self-registration
- Open Administration → Realm Settings → tab Self-Registration.
- Check Enable self-registration.
- Configure the additional fields that appear (see below).
- Save.
Once enabled, the login page picks up a "No account yet? Register →" link and /register becomes reachable.
Fields
| Field | Default | Meaning |
|---|---|---|
| Enable self-registration | off | Master toggle. When off, the /register route returns the same anti-enumeration response as a never-registered email. |
| Require email verification | on | New accounts are created with EmailConfirmed=false and can't sign in until they click the magic-link in the verification mail. Turn off only for trusted-internal scenarios. |
| Require admin approval | off | Layered on top of email verification — after the user confirms the link, the account stays IsActive=false until an admin flips the flag manually. Useful for moderated communities. |
| Allowed email domains | empty (all) | Whitelist. Empty = accept any domain. Case-insensitive match on the part after the last @. |
| Default groups | empty | Groups the new user is auto-attached to once the account is fully active (post-verification + post-approval). Role memberships flow through groups, so this is the lever for "what can self-registered users do?". |
| Terms-of-Service URL | empty | When set: the registration form shows a required "I accept" checkbox linking here. The endpoint rejects submissions without the checkbox ticked. |
| Privacy Policy URL | empty | When set: rendered as a discreet footer link on the registration form. No checkbox. |
| Enable Cloudflare Turnstile captcha | off | Independent of the master toggle. See Captcha below. |
| Captcha site key | empty | Per-realm Turnstile site key. Empty + captcha enabled = falls back to the Cocoar-default keys. |
| Captcha secret | not set | Per-realm Turnstile secret, encrypted at rest. Write-only — never returned, only an "is configured" flag. Empty + save = clear (revert to Cocoar default). |
Captcha
Cloudflare Turnstile is the only supported captcha provider. It is independent of the master toggle so two scenarios both work:
- Public-internet deployment → enable captcha, configure either per-realm keys or rely on the Cocoar-default keys configured via the
Turnstile__SiteKey/Turnstile__SecretKeyenvironment variables. - Air-gapped / intern deployment → leave captcha disabled. Modgud then never calls out to
challenges.cloudflare.com. Honeypot field + per-email rate-limit (1/min, 3/hour) cover the bot-spam surface.
Resolution order when the captcha is enabled:
- Per-realm site key + per-realm secret if both set
- Cocoar-default site key + Cocoar-default secret
- None configured → the verifier rejects every registration and logs a
WARNso the admin notices the misconfiguration
One captcha secret per realm
The captcha secret is encrypted with ASP.NET Data Protection (purpose Modgud.SelfRegistration.CaptchaSecret.v1). Migrating data-protection keys between deployments invalidates all per-realm captcha secrets — they need to be re-entered. The same warning applies to login-provider client secrets.
Anti-enumeration
The public endpoints are explicitly engineered against enumeration:
POST /api/account/registeralways responds with the same generic success message regardless of outcome: existing email, existing username, captcha failure, honeypot trigger, rate-limit, domain-whitelist rejection — all look identical to a real success from the client's perspective. No mail is sent in the rejected cases.GET /api/account/self-registration-inforeturns the same all-defaults shape (Enabled=false) whether the realm has the feature off, doesn't exist at all, or is currently being configured. The SPA readsEnabledto decide between rendering the form vs. redirecting to/login.
The email-verification endpoint (POST /api/account/register/verify-email) is the exception — it returns real error codes for expired / used / unknown tokens, because by the time someone is consuming a token they already have it, and there is nothing left to enumerate.
What's stored where
| Data | Location |
|---|---|
| Realm-settings document (the toggles above) | tenant DB, singleton document RealmSettings |
| Pending self-registration (token-hash, user ID, expiry) | tenant DB, mt_doc_pendingselfregistration |
| User record | tenant DB, mt_doc_applicationuser (created at register time, activated on verify) |
| Captcha secret (encrypted) | inside the RealmSettings document, encrypted with Data Protection |
Known limitations (current MVP)
- No dedicated "pending approvals" UI. When admin-approval is required, the user record is created with
IsActive=falseand an admin has to flip the flag from the regular user-edit modal. A filter chip on the user list for "pending approval" is a sensible follow-up. - In-memory rate limiter. The per-email cap (1/min, 3/h) resets on restart and isn't shared across multiple instances. Single-instance deployments are fine; multi-instance setups can bypass the cap by hopping between instances. A Redis-backed implementation behind the same interface is a follow-up.
- No pre-submit username availability check. The form surfaces username collisions through the generic 200-OK like every other rejection. An anonymous
GET /api/account/check-username/{name}(rate-limited) would improve the UX without touching the anti-enumeration guarantees on email. - Email template is shared with the email-change flow. Both reuse
EmailTemplate.EmailVerification. A dedicatedEmailTemplate.SelfRegistrationVerifywith welcome wording is a quality-of-life improvement.
Dynamic Client Registration
Anonymous OAuth-client registration policy: master toggle, token lifetimes, GC TTL, rate limits, reserved-name blocklist. The companion per-API and per-Scope toggles live on OAuth APIs and OAuth Scopes respectively — DCR is a triple opt-in by design.
Off by default. See the full feature page for when to enable it, what gets accepted, and the consent-screen [unverified] marker:
→ Dynamic Client Registration (full feature page)
Branding (separate page)
Branding lives on its own page under Plattform — go to Plattform → Customization → Branding. It writes a sub-document on the same RealmSettings doc but isn't surfaced as a tab here today.