Nimbusv1.0.0

Auth Module

Dashboard authentication, session TTLs, magic-link + TOTP tuneables.

The Auth module powers dashboard login, sessions, magic links, and TOTP 2FA. Introduced in v0.11 alongside the dashboard RBAC rewrite.

Runtime config lives at config/modules/auth/auth.toml — auto-generated with safe defaults on first launch. The companion AES-256 key file config/modules/auth/session.key is also auto-generated (chmod 600 on POSIX).

The auth module requires the Perms module for Minecraft-account login. Without Perms, only API-token auth works and sessions cannot resolve permissions. See the Dashboard Auth guide for the end-user flow.

Complete example

config/modules/auth/auth.toml
[sessions]
ttl_seconds = 604800        # 7 days
rolling_refresh = true
max_per_user = 10

[login_challenge]
code_ttl_seconds = 60
code_length = 6
code_alphabet = "numeric"        # or "alphanumeric"
magic_link_ttl_seconds = 60
magic_link_token_bytes = 32
magic_link_enabled = true
max_generates_per_minute = 5
max_consume_failures_per_minute = 10

[dashboard]
public_url = "https://dashboard.nimbuspowered.org"

[totp]
issuer = "Nimbus"
require_for_admin = false
window = 1

[security]
token_encryption_key_file = "config/modules/auth/session.key"

[webauthn]
enabled = true
rp_id = ""                          # empty → auto-derive from public_url
rp_name = "Nimbus Dashboard"
origins = []                        # empty → auto-derive from public_url
challenge_ttl_seconds = 300
max_credentials_per_user = 10
allow_origin_port = false           # turn on only for local dev

[sessions]

KeyDefaultNotes
ttl_seconds6048007 days. Session expiry from creation (or last use if rolling refresh).
rolling_refreshtrueExtends expires_at on every authenticated call.
max_per_user10When exceeded, the oldest active session is revoked on new login.

[login_challenge]

KeyDefaultNotes
code_ttl_seconds60How long a 6-digit code stays valid.
code_length6Number of characters in the in-game code.
code_alphabet"numeric""numeric" (0-9) or "alphanumeric" (ambiguity-safe: no 0/O, 1/I).
magic_link_ttl_seconds60Lifetime of magic-link tokens.
magic_link_token_bytes32256-bit URL-safe random token.
magic_link_enabledtrueOperator kill-switch — false forces code-only login.
max_generates_per_minute5Per-UUID rate limit on code/link issuance.
max_consume_failures_per_minute10Per-source-IP brute-force cap on consume-challenge (sliding 60s window). After this many failed attempts further consume calls are rejected with HTTP 429 until the window decays. Set to 0 to disable. Defends the 6-digit code's TTL window. Added v0.11.1.

[dashboard]

KeyDefaultNotes
public_url"https://dashboard.nimbuspowered.org"Base URL used to construct magic-link targets.

public_url also exists in the core nimbus.toml under [dashboard]. The auth module prefers the core value when set — so operators only need to update one file. The module-level key remains as an override for unusual deployments.

[totp]

KeyDefaultNotes
issuer"Nimbus"Shown in the authenticator app entry (Nimbus: <player-name>).
require_for_adminfalseDashboard prompts admins without TOTP to enroll on first login.
window1Clock-skew tolerance in 30-second steps (±1 = accepts 30s past/future).

[security]

KeyDefaultNotes
token_encryption_key_file"config/modules/auth/session.key"32-byte AES-256 key used to encrypt TOTP secrets.

Back up session.key alongside your database. If the key is lost, every TOTP enrollment is unrecoverable — all users will need to re-enroll from scratch. The key is auto-regenerated if missing on boot, so restoring only the DB is not enough.

[webauthn]

Passkey / WebAuthn settings. The module ships with sensible defaults — you only need to edit this block if the dashboard is reachable on a hostname that doesn't match [dashboard] public_url, or if you want to allow additional origins (e.g. http://localhost:3000 for local dev).

KeyDefaultNotes
enabledtrueKill-switch. When false, the Passkey card is hidden in the UI and all /api/auth/passkey/* routes return 404.
rp_id""Relying Party ID — must be the dashboard's hostname exactly, no scheme/port/path. Empty = auto-derive from public_url.
rp_name"Nimbus Dashboard"Human-readable RP name shown in the authenticator prompt.
origins[]Allowed browser origins (https://host[:port]). Empty = auto-derive from public_url. Add http://localhost:3000 for local dev.
challenge_ttl_seconds300How long a registration / authentication ceremony stays valid before the server drops it from its in-memory cache.
max_credentials_per_user10Cap per MC UUID. Keeps the credential table from growing unbounded if a user enrolls a device and never prunes it.
allow_origin_portfalseWhen true, the RP accepts assertions from the same host on any port. Off by default so production RP binding is strict (https://host:443 and https://host:3000 are distinct origins). Turn on only for local dev where the dashboard floats between ports.

Changing rp_id invalidates every existing passkey — browsers bind the credential to the RP ID at registration time, and a mismatch is indistinguishable from a phishing attempt. Only change this if the dashboard's public hostname actually changes, and warn users to re-enroll.

Endpoints

RouteAuthPurpose
POST /api/auth/generate-codeService tokenCalled from the controller-side /nimbus dashboard login command handler to mint a 6-digit challenge.
POST /api/auth/request-magic-linkService tokenCalled from /nimbus dashboard login link to mint a magic-link token.
POST /api/auth/deliver-magic-linkPublic (rate-limited)Dashboard-initiated: user types their MC name, controller fires AUTH_MAGIC_LINK_DELIVERY which the nimbus-auth-velocity plugin turns into a clickable chat component.
POST /api/auth/consume-challengePublicDashboard exchanges a code or magic-link token for a session.
POST /api/auth/consume-magic-linkPublicDashboard consumes a ?link=<token> deep-link on page load.
POST /api/auth/totp-verifyPublicSecond step when TOTP is enabled. Accepts a live TOTP code or a recovery code.
POST /api/auth/logoutBearer sessionRevokes the calling session.
GET /api/auth/ws-ticketBearer sessionMints a single-use, 30-second wt_… ticket the dashboard uses in ?token= when opening a WebSocket or SSE connection, so the long-lived session token never lands in a URL.
GET /api/auth/meBearer sessionReturns {uuid, name, permissions[], isAdmin, totpEnabled}.
GET /api/auth/my-sessionsBearer sessionLists the caller's own sessions with a currentSessionId marker.
DELETE /api/auth/my-sessions/{sessionId}Bearer sessionRevokes a sibling session.
POST /api/auth/my-sessions/revoke-othersBearer sessionRevokes every session belonging to the caller except the current one.
GET /api/auth/sessionsService tokenAdmin view of all sessions (used by the in-game /nimbus dashboard sessions command).
POST /api/auth/logout-allService tokenRevokes every session for a given UUID (used by /nimbus dashboard logout-all).
GET /api/profile/totp/statusBearer sessionReports TOTP state + remaining recovery codes.
POST /api/profile/totp/enrollBearer sessionStarts enrollment — returns secret, otpauth:// URI, and 10 recovery codes.
POST /api/profile/totp/confirmBearer sessionActivates a pending enrollment.
POST /api/profile/totp/disableBearer sessionRequires a current TOTP code or a recovery code.
POST /api/auth/passkey/register/startBearer sessionBegins a WebAuthn registration ceremony. Returns {ceremonyId, publicKeyOptionsJson}.
POST /api/auth/passkey/register/finishBearer sessionCompletes registration. Persists the credential under the caller's UUID with the supplied label.
POST /api/auth/passkey/login/startPublicBegins an authentication ceremony. Discoverable credentials — no username needed.
POST /api/auth/passkey/login/finishPublicCompletes authentication and issues a regular dashboard session (loginMethod = "passkey").
GET /api/auth/passkey/credentialsBearer sessionLists the caller's registered passkeys.
DELETE /api/auth/passkey/credentials/{id}Bearer sessionDeletes a registered passkey.

Service-token endpoints are called by the controller's own /nimbus dashboard ... ModuleCommand handler when a player runs the in-game command (the caller UUID is forwarded from the Bridge plugin via CommandExecuteRequest). The nimbus-auth-velocity plugin itself only subscribes to the AUTH_MAGIC_LINK_DELIVERY module event — it doesn't hold a service token and doesn't call these endpoints directly.

API tokens vs. dashboard sessions

The NIMBUS_API_TOKEN (env var or [api] token in nimbus.toml) is implicit nimbus.dashboard.admin and bypasses TOTP.

This is intentional — it keeps every script, CI job, and SDK call that already uses the API token working without forcing a 2FA dance on machine-to-machine traffic. But it means anyone holding the token has full admin access without going through the MC-account login or the TOTP step.

Treat the API token like a root SSH key:

  • Never commit it to version control.
  • Rotate it whenever someone with access leaves or a machine is decommissioned.
  • Don't share it across operators — give each their own MC-account session instead.
  • If you only need read-only programmatic access, prefer issuing an MC-account session for a low-privilege user and using its bearer.

Audit events (DashboardLoginSucceeded etc., added in v0.11.1) are not emitted for API-token calls — those show up in the regular audit_log under the actor field as api:<name> instead.

Audit events (v0.11.1+)

The auth module emits NimbusEvent.Dashboard* events into the core EventBus, which the AuditCollector persists to the audit_log table:

EventTriggered by
DashboardLoginSucceededSuccessful end-to-end login (post-TOTP if required).
DashboardLoginFailedInvalid challenge, wrong TOTP, or rate-limited consume.
DashboardSessionRevoked/api/auth/logout, sibling-revoke, revoke-others, logout-all.

Each carries the source IP (when known), the affected UUID, and a reason/scope tag. Inspect via audit console command or GET /api/audit.