Skip to content

Authentication

Domekologe edited this page Aug 7, 2026 · 4 revisions

Authentication

🌐 English · Deutsch

Authentication is enabled by default. On first start the admin account is created via a setup token (Getting Started).

Users & roles

Role Permissions
admin Everything — incl. settings, encoding, user management, library management (delete/rename/move), custom paths, AutoSync management
user Search, downloads, own favourites, own notifications, view/play the library
kids Like user, but age-limited and read-only: no downloads, no Auto-Sync, no module store, no module settings

The kids role is a restriction, not a rank below user. An account that has it sees an age-limited app and cannot change that — there is no PIN, because there is nothing to leave. The limit is the one set under Settings → Start Page → Kids mode → Age limit (default 6) and is applied by the server at every point, not by the browser:

Where What happens
Home feed Titles rated above the limit are dropped while the rows are built; the 18+ source is never fetched
Search The adult source answers 403; results are filtered against the cached TMDB rating
Advanced search TMDB is queried with certification.lte, so the paging is already limited
Library The shelf is filtered against the cached TMDB rating
Playback /api/stream/start refuses the file itself — a copied or guessed path does not play either
Downloads /api/download and Auto-Sync answer 403
Module store, module settings Redirect / 403

Be clear about what this can and cannot do. Filtering can only judge a title TMDB has a certification for, and unrated titles are shown on purpose — dropping everything unrated would empty the app on an instance without a TMDB key, and an empty app is one people switch the protection off for. The parts that do not depend on metadata at all — the refusals in the table above — are what actually holds.

Don't confuse it with the kids mode on the home page: that is for a shared account with a child in front of it right now, it is entered by anyone and left with a PIN. The role is for a child with their own login.

User management (create, delete, change role) is done by admins on the settings page. Rules: username max. 64 characters (a–z, 0–9, ., _, -), password min. 8 characters. You cannot delete your own account. Role changes apply without re-login (the session role is refreshed from the DB every 15 s).

Screenshot: user management

Sessions & protections

  • Sessions are valid for 24 h (HTTPOnly, SameSite=Lax; Secure flag with HTTPS).
  • Login rate limit: max. 10 login attempts per minute (optional Redis backend via MEDIAFORGE_REDIS_URL).
  • CSRF protection for forms; JSON API routes require Content-Type: application/json.
  • Security headers (CSP, X-Frame-Options DENY, nosniff, Referrer-Policy; HSTS with HTTPS).
  • Session fixation protection: the session is recreated on every login.
  • The Flask secret key lives in ~/.mediaforge/.flask_secret (file mode 0600).

HTTPS: Behind a TLS reverse proxy, set web_base_url to the https:// URL or export MEDIAFORGE_HTTPS=1 — otherwise session cookies are not marked "Secure" (the app logs a warning).

SSO / OIDC

Single sign-on with any OpenID Connect provider (Keycloak, Authentik, Authelia, …). Configure under Settings → SSO or via environment variables:

Setting DB key Env variable
Enable SSO web_sso MEDIAFORGE_WEB_SSO
SSO only (disable local login) web_force_sso MEDIAFORGE_WEB_FORCE_SSO
Issuer URL oidc_issuer_url MEDIAFORGE_OIDC_ISSUER_URL
Client ID oidc_client_id MEDIAFORGE_OIDC_CLIENT_ID
Client secret oidc_client_secret MEDIAFORGE_OIDC_CLIENT_SECRET
Button display name oidc_display_name MEDIAFORGE_OIDC_DISPLAY_NAME
Admin username oidc_admin_user MEDIAFORGE_OIDC_ADMIN_USER
Admin subject (sub claim) oidc_admin_subject MEDIAFORGE_OIDC_ADMIN_SUBJECT

Notes:

  • The discovery URL is derived automatically from the issuer URL (<issuer>/.well-known/openid-configuration); scopes: openid email profile.
  • Register <base URL>/oidc/callback as the redirect URI at your provider.
  • SSO users are created automatically on first login. If the username or sub claim matches oidc_admin_user/oidc_admin_subject, the user is granted admin rights.
  • Changes to the SSO configuration require a restart.
  • With Force SSO the local login form is disabled entirely.

External API authentication

The /api/v1/... endpoints don't use session login but an API key in the X-Api-Key header. The key is generated automatically on first start and can be viewed and regenerated in the settings (API Reference).

Running without authentication

Internally a no-auth mode exists (virtual admin), but the standard launch always enables authentication. Never expose an instance without auth to untrusted networks.

Groups & permissions

Three fixed roles have no middle: to let somebody approve downloads you had to make them an admin, which also handed them the settings, the module store and every other account.

A group bundles two things that always travelled together anyway: a set of permissions (library.read, queue.write, settings.admin, …) and a library scope — which library locations its members may see at all. Keeping them in one object is deliberate; splitting them means every "why can this person not see that folder?" question has two places to look, and they drift.

Groups are managed in Settings → Authentication → Groups & Permissions, next to the user list.

Built-in groups

admin, user and kids exist as built-in groups matching the three roles. They cannot be renamed, edited or deleted: they define what the roles mean app-wide, the decorators rely on them, and editing them from the UI would let an admin lock themselves out of the settings page that fixes it.

kids is a restriction, not a rank. Note the absence of adult.view — the age gate is expressed as a permission rather than as another role-name comparison scattered through the routes.

How they combine

A user's effective permissions are the union of the built-in group matching their role and every custom group they belong to. That makes groups purely additive: nobody loses access on upgrade, and admin_required keeps working unchanged because admin still holds the wildcard.

Library scope works the other way round: an explicit scope beats the wildcard. Every user is in a built-in group whose scope is *, so if the wildcard simply won, scoping would be permanently dead. As soon as any of a user's groups names concrete locations, the user is restricted to the union of those. Admins are never scoped — an admin who cannot see a library cannot fix it either.

A permission this build does not know is dropped when you save, not stored: a permission nothing checks looks granted in the UI and does nothing, and that is how people end up believing an account is restricted when it is not.

Every group change is recorded in the Audit Log.

Library scoping

A group's scope names the library locations its members may see: default for the download root, or a custom path's id. The group editor lists them by name, so nobody has to remember an id — a scope naming a location that does not exist looks configured and restricts nothing.

Enforcement is at the choke points rather than per route, which is the only way it stays true as routes are added:

  • /api/library and /api/library/overview drop out-of-scope locations, including from the counters. Counting everything and showing some of it is how a "restricted" view leaks exactly what it was meant to hide.
  • lib_resolve_library_file() — the single "may the caller touch this file?" answer, used by delete, rename, media-info, the eBook and comic routes and the upscale queue — refuses paths outside the caller's scope. Background workers pass scoped=False: they have no session, and an upscale job must not fail because the account that queued it was later restricted.
  • The player's _resolve_media_path() applies it too. "Cannot see it in the library but can still stream it by path" is not a restriction.

Scanning deliberately stays unscoped. The index has to cover the whole library regardless of who happens to be logged in when a scan runs; filtering there would make the cache depend on the session that populated it.

Clone this wiki locally