HTTP permissions and browser authentication
BSM keeps the user, moderator, and admin roles. HTTP routes declare
capabilities through Security(require_permissions, scopes=[Permission.SERVERS_CONTROL]).
Every declared capability is required. Grants come from web/access_policy.py;
authentication still looks up the account and its current role. These endpoint
scopes do not restrict an individual JWT. Selecting scopes in Swagger does
not mint a restricted token.
GET /api/account adds a sorted permissions field. The same list is available
at GET /api/account/permissions. User-list responses retain their existing
shape. Permissions describe static capabilities; task ownership, application-log
restrictions, local readiness checks and shutdown credentials still apply.
OpenAPI exposes the scope catalog and x-bsm-access version 1 on operations.
Classifications are public, authenticated, permission, legacy-role,
custom, and unknown. Metadata comes from dependencies and explicit contextual
declarations. Unknown plugin policies are never assumed public. allowed_roles
only describes the static policy, not a guarantee that a request succeeds.
Cookie and bearer security entries are alternatives. Cookie entries have no
OAuth scopes; both transports enforce the same application permissions.
Swagger
Open /docs and use Authorize with the existing OAuth2 password endpoint.
Swagger uses a pinned Swagger UI distribution and a fetch wrapper plus request
interceptor that omit cookies for schema, login, and operation requests.
Authorization persistence is disabled. Authorizing another account therefore
does not overwrite the embedded frontend cookie. Clearing Swagger authorization
leaves protected requests unauthenticated even when a frontend cookie exists.
URLs respect the trusted ingress root path. Tokens are not copied from the SPA.
Plugins
The helpers get_current_user, get_moderator_user, get_admin_user, and
require_permissions are exported from bedrock_server_manager.
A plugin may declare custom HTTP permissions on PluginBase:
class ExamplePlugin(PluginBase):
version = "1.0.0"
http_permissions = [
("jobs:run", "Run example jobs", ["moderator", "admin"]),
]
Omitting plugin: automatically prefixes plugin:<plugin_name>: (for example
plugin:example:jobs:run). Use it in a route with Security(require_permissions, scopes=["plugin:example:jobs:run"]). Names must use the plugin’s own namespace.
Descriptions and explicit role grants are required; collisions and invalid names
are rejected. Registration is owned by the plugin, removed on unload or failed
load, and invalidates OpenAPI. Admin has no implicit bypass for plugin grants.
This does not change trusted in-process Python plugin access. (The legacy
self.api.runtime.register_http_permission() method remains supported for
compatibility but logs a deprecation warning.)
Logout
POST /auth/logout revokes the current login session and closes its live
connections. Cookie mode also clears its cookie. The deprecated GET shim remains
hidden from OpenAPI. Refreshing a JWT retains its session ID and original
absolute deadline. Swagger’s local authorization clear only forgets its bearer.
Account security settings can revoke individual logins, other logins, or every
login. Integration credentials are managed separately and survive session logout.
A client receiving X-BSM-Session-Invalid: 1 must request an explicit sign-in;
the SDK does not silently authenticate again with a saved password.
Account security
The Account page provides Sessions, Integrations, My access, Activity, and Password & MFA tabs. Session labels, user agents, and IPs are informational hints, not proof of device identity. Last activity includes authenticated background polling. Administrators can inspect and revoke another account’s sessions and reset its MFA. Password resets revoke existing logins; changing your own password keeps the verified current session. Integration credentials remain independent.
security.session_idle_hours defaults to 24 (range 1–672), and
security.session_max_days defaults to 28 (range 1–90). Non-remembered logins
last at most 24 hours. Remembered logins are capped by the configured maximum.
Sensitive account and administrator changes require password verification within
five minutes, plus a factor when MFA is enabled. Refresh does not renew this proof.
Authentication attempts are limited per peer and account in five-minute windows;
a successful login resets its account failure budget.
Authenticator enrollment expires after ten minutes. TOTP codes cannot be replayed.
Ten one-use recovery codes are displayed once; save them securely. Replacing them
invalidates the old set. Enabling or disabling MFA revokes other login sessions.
Back up the database together with credentials/mfa.key (relative to the
installation configuration directory). The key encrypts authenticator secrets;
losing it requires recovery codes or an administrator MFA reset. Recovery codes
are stored as hashes. Security activity is retained for 90 days with opportunistic
cleanup; it excludes passwords, bearer credentials, and authenticator secrets.
Upgrading requires one new sign-in: older JWTs without a tracked session are rejected. Database migration creates the session and MFA records automatically.
Named integration credentials
POST /api/account/tokens issues a named bearer credential, returned once as
token. Supply name, explicit permissions, optional immutable server_ids,
and expires_in_days (1–365, default 30). GET /api/account/tokens returns only
metadata; DELETE /api/account/tokens/{token_id} revokes it. Admins can list or
revoke another account’s credentials through /api/access/users/{user_id}/tokens.
There are at most 100 live credentials per account. Only a SHA-256 digest is
stored; the random secret is never stored, logged, or placed in task records.
Last-use timestamps are updated at most once per minute.
An integration credential always intersects its explicit capabilities with the owner’s current role and server assignments. Revocation, expiry, disabling or deleting the owner, and role reductions take effect on subsequent requests and socket deliveries. Integration credentials cannot manage accounts, permissions, or credentials, and cannot exchange themselves for an unrestricted JWT. They must be supplied as bearer headers or the WebSocket authentication frame, never cookies or URL parameters. Swagger’s IntegrationToken bearer scheme accepts a pasted named credential. Logging out of a client discards its local copy; revocation is an explicit, separate operation.
Assigned servers
Existing accounts keep access to all servers allowed by their role. Admins can
use PUT /api/access/users/{user_id}/servers to replace an account’s assignments
with { "mode": "assigned", "grants": [{ "server_id": "...", "permissions": ["servers:read"] }] }. mode: all restores the role’s ordinary server access.
GET reads assignments. GET /api/access/servers lists visible immutable IDs
and effective capabilities; GET /api/access/policy supplies the catalog and
role ceilings. Every server grant must include servers:read.
Assignments narrow access; they never elevate a role. Use moderator or user accounts for scoped operators. Admin accounts and an administrator’s own access cannot be restricted through this endpoint. A token can further restrict any owner’s resources. Server-restricted principals lose host-wide capabilities such as application settings, global backup keys, and application metrics. Public appearance settings and icon assets remain public; these assignments are operational authorization, not a redesign of public presentation data.
Users and servers have immutable identities. Deleting and recreating a name cannot transfer grants or integration credentials. Newly issued JWTs also carry the account identity and a tracked session; older JWTs require a new sign-in. Ordinary server backups do not export account grants or credentials; those reside in the application database.
Changes, tasks, and sockets
GET /api/account includes policy_revision, server_access, effective
server_permissions keyed by immutable ID, and the current credential_id when
applicable. Responses expose X-BSM-Policy-Revision through CORS. Policy revision
changes invalidate frontend account caches and reload the live API explorer.
Socket control messages access_changed, policy_changed, and
credentials_changed contain no secrets. Notifications are advisory; request
and delivery checks read committed access state. Creating a credential does not
reset the session or erase its one-time secret display.
Task records capture the initiating account identity, credential ID, resource, and required capabilities. Restricted principals can only read authorized task results; different integration credentials cannot read each other’s tasks. Socket subscriptions and each delivery enforce the current resource and capability restrictions. Unknown plugin/global topics are unavailable to restricted principals. Legacy plugin routes without explicit permission policies remain available to ordinary unrestricted sessions, but fail closed for integration credentials and assigned-server sessions. Plugins should declare permissions and explicitly authorize each resource in collection endpoints.
Integration credentials can be rotated with
POST /api/account/tokens/{token_id}/rotate. Rotation atomically revokes the old
secret and returns its replacement once, preserving the original absolute expiry
and permission/resource ceilings. Bulk integration revocation is an explicit,
separate action from signing out logins.
Integration credential creation accepts expires_in_days: null for Never
expire; omitting it still defaults to 30 days. Metadata returns
expires_at: null. Such credentials remain subject to revocation, account
activation, current role/server access, and the active-credential limit. Rotation
preserves the lack of an expiration deadline. Downgrading to a release without
this capability expires non-expiring credentials at the downgrade time.
Interactive login-session deadlines are unchanged.
The login screen asks for username/password first. Only after those credentials
are verified does an MFA-enabled account receive a 401 with
X-BSM-MFA: required; this response issues no session or token. The client then
asks for an authenticator or recovery code and resubmits the credentials with
otp. Incorrect passwords never expose whether the account has MFA. API clients
may still supply all three values in a single request.
Authentication hardening
New and reset passwords require at least 8 characters, reject a local list of common passwords and highly repetitive values, and accept up to 1024 UTF-8 bytes. Existing passwords still work at login. New hashes use Argon2id; successful legacy bcrypt logins upgrade their hash without signing out existing sessions. Unknown accounts perform dummy password verification. Validation errors omit passwords.
MFA and account authority changes advance the authentication revision so an in-flight login or verification cannot bypass the change. Role and activation changes revoke login sessions before advisory connection cleanup. Idle-expired sessions do not consume the active-session limit. Authentication attempts clean up old throttle rows and session/security history older than 90 days. Successful recent verification resets its account attempt budget.
Administrators can configure security.auth_account_attempts (default 10),
security.auth_peer_attempts (60), and security.auth_window_seconds (300).
security.require_admin_mfa defaults to false. When enabled, an unenrolled
administrator can only access account enrollment and sign-out until MFA is
confirmed. Named integration credentials keep their independent authorization.
Required administrators cannot disable their own MFA. For a lost factor, an
operator with local configuration/database access can run bedrock-server-manager reset-mfa USER (or add --yes); this clears factors, revokes login sessions, and
records the recovery action, without changing the password or integrations.
Owned security changes produce safe, account-scoped notices for connected
interactive clients and the private on_account_security core event. Notices
contain an event ID, owner ID, action, and timestamp, never credentials or factor
secrets. Delivery is advisory; the security activity log remains authoritative.
Registration invitation storage and concurrency
Registration invitations are stored as SHA-256 digests. Existing invitation URLs remain valid after the digest migration, retain their role and deadline, and can be redeemed only once. Registration and validation requests share the configured peer authentication budget. A throttled registration returns 429 before password hashing or invitation consumption; invalid/expired invitations are also rejected before hashing. Account creation revalidates the invitation within its transaction.
On PostgreSQL/MySQL, invitation consumption requests a row lock with
SELECT FOR UPDATE. SQLite ignores that clause. The supported SQLite application
process serializes account creation with Storage.write_lock, including the
invitation recheck, account insert and invitation deletion in one transaction.
That in-process lock is not a cross-worker/distributed lock: do not run independent
BSM application workers against the same SQLite database. A same-process
concurrency test explicitly coordinates both requests past preliminary validation
and proves that only one account is created.
Migration g83c712e49a0 hashes existing invitation secrets in place and creates
the unique digest index. Downgrading past g83c712e49a0 cannot reconstruct raw
invitation secrets: existing invitations become unusable and must be reissued.
Re-upgrading removes the unusable null-token rows; it does not restore old links.
Take a database backup before schema changes, and protect older backups because
they may still contain plaintext invitations.
Invitation page/API responses use Cache-Control: no-store and
Referrer-Policy: no-referrer, including registration throttling responses. BSM’s
application log formatter redacts invitation secrets from page, validation and
submission URLs, including ingress-prefixed paths. Configure reverse-proxy access
logs separately: BSM cannot redact logs written by another service. When serving
the frontend separately, apply equivalent cache/referrer rules on registration
page responses at that frontend host; backend headers do not configure it.
Authentication maintenance and request ownership
JWT authentication resolves a persisted account, validates its login family and
builds current grants once for that request or WebSocket handshake. The internal
AuthenticatedSession carries the resulting identity and authorization; it is
not a cross-request cache. Self-service security endpoints reuse the actor’s
owner ID from the request context, while administrator operations still look up
the target account. The permission dependency owns the recent-authentication
check for access mutations. Token creation and rotation retain their separate
recent-authentication checks.
Every subsequent protected request and WebSocket frame checks committed state. A role change closes the old socket identity. Outgoing WebSocket delivery still refreshes authorization independently under the send lock and filters the payload again. An already executing request is not retroactively cancelled. Password verification remains outside database transactions; session issuance still checks account activity and the authentication revision after verification.
Login and renewal share the pure lifetime policy: 24 hours without remember-me,
otherwise web.token_expires_weeks (four weeks if malformed). Persisted session
maximum and idle limits remain authoritative; renewing never extends a family’s
absolute deadline. Invalid/non-finite legacy settings cannot overflow a token.
Application startup owns an authentication-retention task in HTTP and other
runtime modes. It runs immediately, daily after success and five minutes after
an observable failure. Each transaction deletes at most 250 rows and yields
between full batches. It removes throttle windows older than one day, security
activity older than 90 days and login families whose absolute expiry is over
90 days old. It never deletes integration credentials, including non-expiring
credentials. General audit-log retention is independent. Login, session creation
and activity reads no longer perform retention deletes. Shutdown cancels and
awaits maintenance before closing the database. SQLite deployments still use the
existing supported process model; this task adds no cross-worker locking promise.
Reproducible workload measurements
Run BSM_AUTH_BENCHMARK=1 python -m pytest -s tests/web/test_auth_workload.py with
the same interpreter, hardware and empty SQLite fixture before and after a change.
The opt-in test records statement, write and transaction counts, elapsed time and
owned write-lock wait time, without recording SQL parameters or credentials.
It covers successful/failed login, an enrolled MFA challenge, JWT and integration
requests, WebSocket receive/reply and delivery, and eight concurrent JWT requests.
Write-lock waits cover the application’s owned lock, not database-driver waits;
concurrent wall time includes driver contention. Production database contention
and p95 latency require representative deployment measurements.
An illustrative local run against the registration-hardening parent gave:
Workload |
Statements before → after |
Transactions before → after |
Writes before → after |
|---|---|---|---|
Successful login |
28 → 22 |
16 → 14 |
13 → 8 |
Failed login |
18 → 13 |
10 → 8 |
8 → 4 |
MFA challenge |
13 → 9 |
8 → 7 |
6 → 2 |
JWT request |
5 → 4 |
4 → 3 |
0 → 0 |
Integration request |
6 → 6 |
2 → 2 |
1 → 1 |
WebSocket subscribe and reply |
12 → 9 |
8 → 6 |
0 → 0 |
WebSocket delivery |
6 → 6 |
4 → 4 |
0 → 0 |
Eight concurrent JWT requests |
40 → 32 |
32 → 24 |
0 → 0 |
These are workload-specific observations, not a fixed query budget or a latency claim. Fail-closed authorization, revocation and factor replay protection take priority over reducing any count.
Trusted clients and browser response policy
Ingress checks the immediate peer against web.forwarded_allow_ips before
Uvicorn resolves forwarding headers. Authentication budgets use that resulting
ASGI client address, never a separately parsed X-Forwarded-For. Equivalent IPv6
forms and IPv4-mapped IPv6 addresses share a canonical budget. By default,
security.auth_ipv6_prefix=128 keeps distinct IPv6 clients separate. Operators
may choose a shorter prefix (48–128) to group addresses, accounting for shared
networks; account budgets continue to apply across all addresses. Untrusted
forwarding headers cannot select the ingress path or a different client budget.
Backend responses set X-Content-Type-Options: nosniff,
Permissions-Policy: camera=(), microphone=(), geolocation=(), and default to
Referrer-Policy: strict-origin-when-cross-origin. Invitation routes retain the
stricter no-referrer and no-store policy. Clipboard and public-key credential
(WebAuthn) features remain enabled.
No frame restriction is imposed by default, preserving cross-origin Home
Assistant embedding. Configure web.frame_ancestors with explicit CSP sources,
for example 'self' https://home.example, to restrict embedding. Setting it to
'self' alone prevents a separately hosted Home Assistant from embedding BSM.
No unconditional X-Frame-Options header is added.
web.csp_report_only optionally sends Content-Security-Policy-Report-Only.
Start with reporting, then inspect login, clipboard copy, Swagger’s CDN assets,
plugin pages, theme assets, remote-instance HTTP calls and WebSocket connections
under standalone and ingress URLs. Add a suitable reporting endpoint only where
the deployment provides one. This option does not enforce a source policy.
A frontend hosted separately must configure equivalent headers at its own host;
backend headers do not cover standalone HTML. Do not enforce a blanket CSP
without testing the deployment’s origins, extensions and embedded integrations.
Remembered second-factor devices
After a successful authenticator or recovery-code check, an interactive login or
password reauthentication can opt into remember_device. This issues a separate,
revocable credential valid for 30 days. It can replace the OTP check at login
and during five-minute security reauthentication, including recovery-code
replacement. The current password remains required. A remembered credential
cannot authenticate by itself or extend its own expiry; renewal requires a fresh
OTP or recovery code. An explicitly supplied incorrect OTP never falls back to
remembered trust. Remember devices only on browsers or clients you control.
Cookie-mode clients receive the proof in an HttpOnly, SameSite cookie using the
existing HTTPS/ingress cookie policy, scoped per account. Bearer clients receive
trusted_device_token and trusted_device_expires_at only when a fresh grant is
issued. They must deliberately store the proof as a secret scoped to the backend
and account, then supply it on password login or via
X-BSM-Trusted-Device-Token on account-security requests. Bearer login does not
implicitly consume ambient device cookies. Responses are not cacheable. Only a
SHA-256 digest is stored on the server; device listings contain metadata only.
Account Security → Sessions lists remembered devices separately from login sessions and supports forgetting one, others, or all devices. Forgetting trust does not sign out a session, but invalidates recent verification derived from that device. Independent fresh-OTP verification remains valid. Expired or revoked proofs are checked again at use, rather than cached as authorization decisions. Password, role, account-status and MFA changes invalidate device grants through the account security revision. Recovery-code replacement carries forward only the presented, valid current device with its original expiry and invalidates all other remembered devices. At most 20 current grants are retained; adding another replaces the oldest. Old metadata is bounded and expired records are eligible for the existing background retention process after 90 days.
Migration g83c712e49a0 adds the trusted-device, passkey, and WebAuthn challenge
tables along with recent-verification provenance. Downgrading removes device and
passkey grants; accounts and passwords remain intact.
Access previews, retention, and execution boundaries
POST /api/account/tokens/preview accepts the same body as credential creation.
It returns requested_permissions, effective_permissions,
unavailable_permissions, server_permissions keyed by immutable server ID,
and policy_revision. It creates no credential. The preview applies the current
role, account assignments, non-delegatable permissions, and proposed server list.
It can explain an unavailable permission that creation would reject. A preview
is advisory; creation and every authenticated request still enforce current access.
Administrators can preview proposed assignments with
POST /api/access/users/{user_id}/servers/preview, using the same body as the
assignment update. This shows which proposed grants the target role can actually
exercise. Saving a grant never raises the account’s role ceiling.
Integration credential listings retain their array response and accept limit
(default 100, maximum 500) and offset (default 0). The same pagination applies
to administrators listing another account’s credentials. Rows are ordered by
creation time descending, then ID descending. Offset pages can shift when new
credentials are created. Revoked or expired credentials become eligible for
background removal after 90 days; active non-expiring credentials are retained.
Deleting an account atomically removes its credentials, login sessions, MFA
material, remembered devices and server assignments. Security and administrative
audit events retain their existing history policy. Maintenance also removes
historical orphaned secrets and grants in bounded batches.
HTTP-submitted background work revalidates its credential, effective permissions, and original server identity immediately before execution, after waiting for a concurrency slot. Revoked credentials or removed capabilities cancel queued work. Server deletion and recreation under the same name cannot redirect an accepted task. Work already executing keeps the operation’s existing safe cancellation and recovery behavior; an access change does not interrupt a restore midway.
Application log history requires logs:read_application; server log history
requires logs:read for the selected server. This matches socket topic access.
The OpenAPI x-bsm-access.credential_alternatives field describes conditional
capabilities and dedicated local credentials. Swagger displays these alternatives;
OAuth security entries carry the capability required for each alternative.
Interactive backup emergency-kit downloads require recent password/factor
verification and use the existing X-BSM-Recent-Auth challenge. Integration tokens
remain eligible only when explicitly delegated backups:keys with global access;
this capability includes exporting secret recovery credentials. Server-restricted
tokens cannot use global key routes.
All authentication and setup responses use Cache-Control: no-store and
Pragma: no-cache, including ordinary bearer login and failures. MFA key files
use the same private-path policy as backup credentials: owner-only POSIX modes,
or a protected Windows DACL for the service identity, administrators and SYSTEM.
Existing key files are checked and protected when opened.
Auth audit follow-ups: management and verification
Integration credential lists accept bounded limit (1–500), offset and status
(all, active, inactive) queries. The API keeps its array response and its
all default; the account UI defaults to Active and pages 50 records at a time.
Expired and revoked history is separate, so an old active credential remains
individually discoverable. CLI users can use auth tokens list --status inactive --limit 50 --offset 50. Offset pages may shift during concurrent changes.
Session and remembered-device metadata prioritize active entries before applying
their existing history bounds.
Credential creation and account server-assignment forms show an advisory preview of effective capabilities, unavailable capabilities, affected servers and policy revision. Invalidated/obsolete preview requests are aborted. The write always checks current permissions again; the preview is not a saved authorization grant. Server-assignment previews are explicitly read-only; saving administrative grants still requires recent verification. API Explorer displays conditional credential rules, including server/application logs and dedicated supervisor credentials.
MFA verification reads a versioned factor snapshot in a short transaction, does key-file protection/read/decryption outside it, then conditionally commits using the factor version, encrypted secret and active owner’s security revision. Enrollment also rechecks its pending secret and deadline at commit. Concurrent TOTP or recovery-code use has at most one winner. This deliberately preserves security checks even if it increases a SQL statement count; it does not promise an exclusive-write-lock duration or immediate cancellation of already-running work.
New and reset passwords keep the eight-character minimum. The packaged offline
SecLists common-password dataset supplements the local application-specific
rejections. Password checks never send passwords to SecLists or another service.
The dataset, MIT license, immutable upstream commit, entry count and SHA-256 are
under bedrock_server_manager/data/common-passwords* and included in distributions.
Maintainers should review the list before security releases and periodically
against upstream. Run python scripts/update_password_blocklist.py --check to
verify bundled provenance without network access. To refresh, review upstream
licensing and a specific commit, run the script with --revision <40-character commit>, review the diff, and commit all data/license/provenance files together.
This is a common-password screen, not a complete breached-password database or an
entropy estimate. Existing login passwords are not revalidated against a new list.
Remembered-device events refresh account security views and have fixed, non-secret
notification messages. Revocation records contain the affected device ID/mode and
count; replacement records identify replaced grants. SDK/CLI requests have distinct
client hints, which are descriptive metadata and never proof of hardware identity.
auth devices list and auth devices forget <id>, --others or --all manage this
trust independently of login sessions. CLI setup stores the new account label;
remembering reauthentication resolves the authenticated account before saving the
proof. Environment credential overrides do not implicitly reuse a saved owner’s
remembered proof. The frontend offers a fresh-authenticator choice for deliberate
renewal; trust alone never renews the original deadline.
Recovery codes can be copied explicitly with inline success/failure feedback and a manual-selection fallback, alongside file download. Clipboard actions never run automatically or store codes in notification history. Automated picker/fallback tests do not certify native Windows Save As behavior: verify save/cancel, slow destinations and ingress/fallback on the affected Windows/browser installation.
The opt-in workload test now includes successful TOTP/recovery-code login,
remembered-device creation/login/password step-up, invalid/expired/revoked trust,
and mixed concurrent login/reauthentication. BSM_AUTH_BENCHMARK_SAMPLES selects
1–5 samples per repeatable workflow (default 3); one-use TOTP/grant setup and socket
cases are measured once. Output includes raw count/timing samples and diagnostic
median/p95 summaries, never credentials or SQL parameters. Small-sample quantiles
are not production p95 estimates. The fixture raises only its own test throttle
budgets. SQLite is measured here; PostgreSQL/MySQL deployments require their own
representative driver, contention and isolation measurements before making
cross-process/concurrency claims.
Router audit follow-up, passkeys, and host credentials
Host secret credentials live in <config_dir>/credentials/ using the .key
extension (backup.key, jwt.key, and mfa.key), separate from application
settings. Each file and the containing directory use owner-only POSIX permissions
or protected Windows ACLs. Private-path symlinks are rejected. The initial JWT
signing key file (<config_dir>/credentials/jwt.key) is published atomically
under an OS-held lock. An initialized host with a missing, corrupt, or unreadable
key fails clearly; it never silently rotates the key. The validated key is retained
in the owning Settings instance for the process lifetime; replacing files is not
a supported live key-rotation interface.
Keep <config_dir>/credentials/ with the configuration volume when moving or
recovering an installation. Server backup archives do not carry installation JWT
or MFA keys. Restoring just the database does not restore host credentials. Stop
the application before restoring a matching database/configuration pair. Generic
settings writes cannot change the signing key or its initialization marker, and
HTTP settings reads/forms never expose either field.
WebAuthn passkeys
Accounts can register up to 20 active WebAuthn passkeys (POST /api/account/security/passkeys/register/options
and POST /api/account/security/passkeys/register/verify) after recent step-up
verification. Passkeys support:
Passwordless sign-in (
POST /auth/passkey/optionsandPOST /auth/passkey/verify, operation IDsbegin_passkey_loginandcomplete_passkey_login) withuserVerification="required", satisfying both primary and second-factor requirements (includingsecurity.require_admin_mfa) when the authenticator assertsUV.Step-up reauthentication (
POST /api/account/security/passkeys/verify/optionsandPOST /api/account/security/passkeys/verify, operation IDsbegin_passkey_step_upandverify_account_security_passkey) for five-minute sensitive account/admin operations.Self-service and admin management: users can list (
list_passkeys), rename (rename_passkey), and revoke (revoke_passkey) their own passkeys; administrators can inspect another user’s passkeys (list_account_passkeysatGET /api/access/users/{user_id}/passkeys) and clear them as part ofreset_account_mfa.
Settings authorization follows the setting’s storage scope, including fields introduced by plugins:
Scope |
Read |
Write |
|---|---|---|
Plugin-owned settings table |
|
|
Installation/global settings and |
|
|
Server settings and |
|
|
Generic global HTTP settings exclude the separate plugin-settings table and reject writes to that table, including legacy aliases. Plugin-owned values remain available through the plugin endpoint. Scoped plugin GET/PUT endpoints use the same permissions as their corresponding global/server settings; selecting a server never bypasses its assignment or token boundary. Plugin-defined server/global namespaces are not classified as private merely because a plugin declares them. Plugins must not place secret credentials into an ordinary readable field; use protected plugin files or an explicitly designed secret-field interface when confidentiality is required.
Addon inventory reads use content:read, while mutations retain content:manage.
Read-only inventory consumers receive icon availability without absolute host paths;
existing management consumers retain their full inventory. Bundled global content
uploads require content:manage; bundled download pages/files select backups:read
with server checks or global content:read according to their validated request.
The development event-sender and dynamic-page plugins require plugins:manage.
Legacy third-party role dependencies retain their compatibility behavior.
All core tags are declared in the central catalog. Access metadata includes recent verification and ownership conditions; administrative advisory previews are explicitly marked read-only and do not consume mutation step-up. Actual mutations still require recent verification. Plugin registration preflights duplicate operation IDs and normalized method/path bindings before mounting; existing generated plugin IDs are frozen verbatim, and new IDs should use stable namespaced snake_case. The legacy GET logout operation remains hidden and unchanged. Registration/logout/key-info and panorama responses have concrete success or binary content declarations. Client contracts are regenerated without renaming existing core operation IDs.
Passkey proof and concurrency guarantees
Passkey registration, passwordless login, and passkey step-up request and verify
user verification (PIN/biometric). A presence-only assertion cannot replace a
password or satisfy administrator MFA. Accounts with passkeys but no authenticator
app must use their passkey for sign-in and sensitive verification; password-only
attempts return X-BSM-MFA: passkey. Accounts with TOTP retain password plus
OTP/recovery-code/remembered-device authentication.
Enrolling a passkey signs out other sessions, retaining the enrolling session. Revocation invalidates in-flight factor proofs and recent-verification grants, without automatically revoking unrelated established sessions. Challenges are consumed by a conditional database write; concurrent zero-counter assertions cannot reuse a challenge. Session creation and step-up recheck the proof at the write boundary. Secret-free login activity records the authentication method.
Auth request bodies are bounded to 64 KiB before parsing, including chunked
requests. Encoded WebAuthn fields and RSA/signature sizes are bounded, and
verification uses a limited worker pool. Passkey orchestration lives in
services/passkey_service.py; persisted session lifecycle remains in
services/session_service.py. Existing HTTP operation IDs are unchanged.
Session rows persist the authentication method and whether a second factor was
verified (defaulting to unknown and false rather than assuming proof from
current enrollment). An unverified session on an account with an enrolled factor
must sign in again; unaffected password-only accounts and named integration
credentials keep their existing behavior.