Authentication
Built-in accounts, OIDC, and forward auth can run side by side. Single sign-on is part of the free core.
| Method | Use when |
|---|---|
| Built-in accounts | You want local usernames and passwords without an identity provider. |
| OIDC | You have an identity provider and want Lumastra to handle the login flow directly. |
| Forward auth | Your reverse proxy already authenticates users through an identity provider. |
The iPhone, iPad and Apple TV apps work with all three: approve their device code in a signed-in browser.
Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.
First run: the setup wizard
Section titled “First run: the setup wizard”An empty database opens /setup. Its first screen, Check your install, shows what
the server found before any account exists: the database, Valkey, the optional sidecars
(sonic embedder, Live TV stream proxy), hardware transcoding and FFmpeg, whether the
config volume keeps your settings, your reverse proxy and HTTPS, and the running nodes.
Items marked with a warning are worth fixing first: a config folder that is not a
mounted volume loses settings on restart, and a proxy missing from TRUSTED_PROXY_IPS
breaks forward auth on the next screen. Nothing on that screen blocks setup;
Recheck reads everything again after you fix something, and Copy as text gives
you a summary to paste into a support request.
The wizard then creates the first admin. Single sign-on is recommended and is pre-selected: OIDC, or forward auth when the wizard sees an identity from a trusted reverse proxy. Built-in login is one click away if you have no identity provider.
- OIDC: configure the provider first; the first sign-in must belong to an
ADMIN_GROUPSgroup, otherwise no admin is created. - Forward auth: the forwarded user must belong to an
ADMIN_GROUPSgroup. - Built-in: choose a username and password; this enables
AUTH_NATIVE_LOGIN_ENABLED.
The next step is titled Your account is ready. Now the basics. Everything on it is optional and can be changed later in the admin settings, and it only shows what is wrong, such as a background worker that does not answer or a configured service that is down. Then you add your media folders and, if you like, connect Audiobookshelf. See Quick Start.
Setup stays locked after the first admin is created, even if the install later loses
all admins. Restore the user’s admin group at the IdP (effective on their next
forward-auth request or OIDC sign-in), or set SETUP_UNLOCK=1 in the container
environment and restart. This reopens /setup only while no admin exists. Unset it
once recovery is complete; settings.json cannot unlock setup. Local/dev recovery is
also documented in scripts/setup-unlock.ts.
Built-in login
Section titled “Built-in login”Lumastra stores accounts itself; users sign in at /login, and an admin creates
and manages accounts in Admin → Users. No external service is involved.
-
Enabled by
AUTH_NATIVE_LOGIN_ENABLED(set automatically when you create the first admin via the built-in path; you can also set it in env or admin settings). -
Session lifetime is controlled by
NATIVE_AUTH_SESSION_TTL_DAYS(default 30). -
Sessions are database-backed, so they work across replicas. There’s no email-based password reset yet: an admin resets passwords from Admin → Users.
-
Locked out of the only admin account? Set a new password from the host. It prints a generated password and signs that user out everywhere:
Terminal window docker compose exec lumastra npx tsx --tsconfig tsconfig.scripts.json scripts/reset-password.ts <username>To choose the password yourself, add
-e LUMASTRA_NEW_PASSWORD='…'afterexec.
OIDC single sign-on
Section titled “OIDC single sign-on”Lumastra acts as an OpenID Connect relying party (Authorization Code + PKCE). When
OIDC is enabled, the /login page (and the setup wizard) show a “Sign in with
<provider>” button; the rest of the flow is your identity provider’s. An OIDC
session is the same lumastra_session cookie a built-in login produces, so everything
downstream behaves identically.
Enable it with three variables (it’s off by default):
AUTH_OIDC_ENABLED=trueAUTH_OIDC_ISSUER=https://idp.example.com/application/o/lumastra/AUTH_OIDC_CLIENT_ID=your-client-idAUTH_OIDC_CLIENT_SECRET=your-client-secret # omit for a public (PKCE-only) client- Redirect URI to register at your provider:
https://your-domain/auth/oidc/callback(auto-derived fromORIGIN; override withAUTH_OIDC_REDIRECT_URIonly if needed). - Admin is granted by the groups claim:
AUTH_OIDC_GROUPS_CLAIM(defaultgroups) matched case-insensitively againstADMIN_GROUPS, same as forward auth. Group sync is promote-only: it can grant admin but never silently removes it. - Account linking: a first OIDC login auto-links to an existing account only when
the provider asserts a verified email matching just one account
(
AUTH_OIDC_LINK_BY_VERIFIED_EMAIL, on by default; never links on an unverified email). Linking by username is off by default (AUTH_OIDC_LINK_BY_USERNAME).
For a full, provider-by-provider walkthrough (Authentik, Keycloak, Google, and a local Dex test stack), see Single sign-on with OIDC.
Forward authentication
Section titled “Forward authentication”Use this if you already run an identity provider and want SSO without per-app OIDC clients. Identity arrives in HTTP headers from your reverse proxy, and Lumastra trusts them only when forward auth is turned on and only from known peers.
Forward auth is off by default. Turn it on with:
FORWARD_AUTH_ENABLED=trueIt can only be set as an environment variable (in .env or the container environment),
like TRUSTED_PROXY_IPS. Admin → Settings shows its value but can’t change it, and a
value in settings.json is ignored with a warning in the startup log.
While it’s off, Lumastra ignores the identity headers from every address, so setting
TRUSTED_PROXY_IPS to get real client IP addresses can never sign anyone in. The setup
wizard tells you when your proxy sends identity headers while forward auth is off.
Scroll, swipe, or click and drag to explore. Use the arrows to move, or fit the diagram above.
Users are auto-provisioned on their first authenticated request. You don’t pre-create accounts; you manage users and their groups in your identity provider. Lumastra integrates with Jellyfin and matches usernames case-insensitively, so a forwarded username should match the corresponding Jellyfin username (case need not be identical).
Proxy trust: fail-closed
Section titled “Proxy trust: fail-closed”Because identity arrives in plain HTTP headers, Lumastra only trusts them from known
peers, and only with FORWARD_AUTH_ENABLED=true:
- Set
TRUSTED_PROXY_IPSto your reverse proxy’s address, or the address of every proxy in a chain. Required in production. List proxies only: with forward auth on, every address in a range such as192.168.0.0/16can send identity headers and sign in as anyone. The Security Self-Check warns about IPv4 ranges wider than/24. See Reverse proxy. - Or set
LUMASTRA_TRUST_ANY_PROXY=trueon a single private host where the container isn’t directly reachable.
The address Lumastra checks is the TCP peer it sees, which isn’t always your proxy’s
own address. If the proxy runs on the Docker host and reaches the app through a
published port, the peer is the Docker network’s gateway (for example 172.21.0.1), and
trusting that trusts every process on the host. Run the proxy on the same compose
network instead; see Deploy with Docker → Reverse proxy.
If headers are rejected, the setup wizard and the logs (identity headers ignored — untrusted TCP peer) name the peer address that sent them.
A trusted proxy must also remove the identity headers a client sends. With forward auth
on, Lumastra believes them from every address in TRUSTED_PROXY_IPS, so a proxy that
passes a client’s X-Forwarded-User through lets anyone sign in as anyone. The setups
in the Reverse proxy guide remove them. With
forward auth off, Lumastra ignores and logs such headers, and npm run doctor and the
admin health page warn about them.
The header names are configurable: USERHEADER (default X-Forwarded-User) and
GROUPHEADER (default X-Forwarded-Groups). An optional third header,
NAMEHEADER (default x-authentik-name), carries the user’s human-readable
display name. If your proxy sends it, other users see it (for example, in “who else
watched this”); leave it blank and Lumastra keeps showing the username. NAMEHEADER
is trusted on the same terms as USERHEADER (forward auth on, TRUSTED_PROXY_IPS only).
nginx drops a header whose value is empty, so if your proxy always sends the groups
header (Authentik’s outpost does), a user with no groups can be read as still having
their previous groups (and admin rights). Either add the nginx map from the SSO
guide (recommended), or turn on FORWARD_AUTH_GROUPS_ALWAYS_SENT (off by default) so
a user with no groups is read as “no groups” instead. See
Setting up Authentik for the full mechanism.
Setting it up
Section titled “Setting it up”For a concrete walkthrough with Authentik (provider, reverse-proxy config, and the exact Lumastra env), see Single sign-on with Authentik.
For local development only, set DEFAULT_USER=<name>: in dev mode, requests with no
auth header fall back to that user. A devUser cookie override lets you switch users
without restarting. Both are gated on the build-time dev flag and never active in a
production build.
How the methods coexist
Section titled “How the methods coexist”All three can be on at once. For each request, Lumastra resolves identity in this order:
- A valid session cookie (built-in or OIDC) is honored first.
- Otherwise, forward-auth headers from a trusted proxy are used, if
FORWARD_AUTH_ENABLED=true. - Otherwise, browser visits are sent to
/login(where the password form and/or the OIDC button appear, depending on what’s enabled), or, if only forward auth is configured, to your identity provider.
One safety rule: if a trusted proxy asserts a different username than the cookie, the stale cookie is cleared and the proxy wins (so a proxy-level logout takes effect even with a lingering session).
Security considerations
Section titled “Security considerations”Forward auth: trust the network path
Section titled “Forward auth: trust the network path”Identity headers are unsigned. Turn forward auth on only if you use it, restrict access to the app port, and trust only the proxy’s TCP peer, as described above. Otherwise a caller can forge an admin identity. MFA, password policy, lockout, and central logout are handled by your IdP; Lumastra stores no forward-auth password or session cookie.
Built-in login: the app is the credential custodian
Section titled “Built-in login: the app is the credential custodian”Passwords use scrypt hashing. Login is rate-limited per IP, with per-account lockout
and a server-side password policy. Sessions are database-backed, stored as hashes,
and revoked on password change. Cookies are HttpOnly, SameSite=Lax, and Secure
when the browser uses HTTPS (see the note above). Cross-site form posts are blocked by
an origin check.
There is no built-in MFA or email password reset. Use strong passwords; admins reset user passwords at Admin → Users, or use the host command under Built-in login for admin recovery.
OIDC: standards-based delegation
Section titled “OIDC: standards-based delegation”OIDC verifies PKCE, state, nonce, and the ID-token signature, issuer, audience, and expiry. Login transactions are single-use, and redirects stay on the same origin.
Keep verified-email account linking enabled and username linking disabled unless
you understand the implications. Never set AUTH_OIDC_ALLOW_INSECURE in production:
it disables TLS verification for OIDC traffic. The client secret is readable by admins
in Settings. Set ORIGIN for secure session cookies.
Admin access
Section titled “Admin access”Admin is granted by any of:
- Forward auth: membership in a group named in
ADMIN_GROUPS(defaultadmin, matched case-insensitively); assign the group in your IdP. - OIDC: membership in an
ADMIN_GROUPSgroup via theAUTH_OIDC_GROUPS_CLAIMclaim (some providers need a scope/mapper to emit it; see the OIDC guide). - Built-in login: the first admin is created by the setup wizard; admins promote other accounts in Admin → Users (a manual promotion pins the user, overriding IdP groups).
Offboarding
Section titled “Offboarding”Disable the account at Admin → Users to cut off sessions, device/refresh tokens, PATs, service tokens, and Jellyfin/Subsonic sessions at validation time. Credential revocation is additional best-effort cleanup; failures are reported in the disable result and logs. Re-applying disable is safe.
Removing IdP groups alone depends on correct empty-group header handling. Without it, a proxy that drops empty headers can leave stored groups and admin rights unchanged. See Removed-from-every-group users.
Programmatic & TV access
Section titled “Programmatic & TV access”Apps and scripts use bearer tokens regardless of how browser users sign in.
The two URL surfaces are kept separate intentionally: /api/* stays behind the
forward-auth proxy (the IdP checks every browser request); /api/v1/* is bypassed
by the proxy so token clients are never redirected to a login page.
| Route | Auth | Use |
|---|---|---|
/api/* | Browser session (built-in / OIDC) or forward auth | Browser UI only |
/api/v1/* | Bearer token (JWT / PAT / service) | TV apps, scripts, automation |
/oauth/* | Public | Device authorization flow |
/oauth/links/* | Browser session (built-in / OIDC) or forward auth | Account-linking callback (MyAnimeList) |
/j/* | Emby token / api_key param / jellyfin_compat PAT scope | Jellyfin-compatible clients |
/subsonic/* | Subsonic u+t/s, p, or apiKey params | Subsonic/OpenSubsonic clients |
Token kinds
Section titled “Token kinds”Three token kinds are accepted on /api/v1/*. The middleware routes validation by
prefix:
| Kind | Prefix | Issued by | Feature flag | Notes |
|---|---|---|---|---|
| Device flow JWT | (none: compact JWT) | POST /oauth/token | FEATURE_DEVICE_AUTH | Short-lived (~1 h, HS256); carries device_session_id. Refresh token rotates on every use. |
| Personal Access Token | pat_ | User: settings UI or POST /api/v1/me/pats | FEATURE_USER_PATS | Scoped; max 10 per user; tracked (last-used IP + timestamp). |
| Service token | svc_ | Admin: POST /api/v1/service-tokens | FEATURE_DEVICE_AUTH | Path/method-gated; optional act-as-user impersonation; IP allowlist. |
| Playback grant | (opaque, sent as ?grant=) | POST /api/v1/play-sessions | (always on) | Bound to one item and one playback session, expires (24 h for video and audiobooks, 6 h for music and Live TV), valid only on media URLs. Pinned to the requester’s IP unless the client asked for a cast target (castTarget) and STREAM_TOKEN_WILDCARD_IP allows it (default: allowed). STREAMING_SESSION_ENFORCE_IP=false turns IP pinning off entirely. Replaces stream_ tokens and api_key in media URLs. |
All token values are shown once at creation and stored only as SHA-256 hashes. A
signing secret is required for device JWTs and PATs. Set JWT_SECRET
explicitly, or leave it unset and Lumastra derives one deterministically from
LUMASTRA_MASTER_KEY (stable across restarts and replicas, so tokens keep working
without extra configuration). If you do set it explicitly, keep the value identical
everywhere: changing it invalidates every outstanding device and PAT token.
Scope taxonomy
Section titled “Scope taxonomy”PATs and service tokens accept the grants read, write, and admin. Service tokens
also accept integrations:read (see below); PATs also accept either exclusive
Jellyfin-compatibility scope. Fine-grained scopes such as media:read or tokens:admin
are internal gate names; they cannot be minted. Every operation in the API reference
names the grant it needs (x-required-grant). The grants expand as follows:
read:media:read,sessions:read, andrequests:read. Other read scopes are not granted byread.write: read plus writable scopes; excludestasks:run,webhooks:write, and admin scopes.admin: all scopes.
See API & tokens for the full scope reference. The
jellyfin_compat scopes remain exclusive to /j and are not included in these aliases.
Device Authorization flow
Section titled “Device Authorization flow”Device authorization is enabled by default (FEATURE_DEVICE_AUTH). Clients need no
registration or secret: the user approves a device code at /device, and the client
receives a roughly one-hour access token plus a rotating refresh token. Replaying a
consumed refresh token revokes the device session.
Set FEATURE_DEVICE_AUTH=false to disable it. Upgrades from the old disabled default
turn it on unless you explicitly pin the flag. See
feature flags.
For endpoint parameters, expiry settings, polling limits, and error handling, see OAuth device-flow clients.
Personal Access Tokens
Section titled “Personal Access Tokens”Users create PATs in Settings → Account → API tokens (or via
POST /api/v1/me/pats) once an admin turns on User PAT Creation under
Admin → Settings → Access → Public API & Device Auth (FEATURE_USER_PATS, off by
default). The toggle appears only while the Public API (FEATURE_DEVICE_AUTH) is on;
with it off, set FEATURE_USER_PATS through the environment. Token creation needs a
signing secret (explicit JWT_SECRET or one derived from the master key).
FEATURE_DEVICE_AUTH matters only for where a minted PAT can authenticate: without
it, a PAT still works against the /j Jellyfin-compatible surface, but is rejected on
/api/v1/*. Turn both flags on to use PATs against /api/v1/*. Each PAT carries
explicit scopes; only admins may request admin scope. Maximum 10 active PATs per
user.
Manage PATs:
| Method | Endpoint | Auth required |
|---|---|---|
GET | /api/v1/me/pats | User session or bearer |
POST | /api/v1/me/pats | User session or bearer |
DELETE | /api/v1/me/pats/{id} | User session or bearer |
Service tokens
Section titled “Service tokens”Admins create service tokens (POST /api/v1/service-tokens) for automation and IaC.
Service tokens support:
- A grant list (
scopes):read,write,admin, orintegrations:read. - Optional IP allowlist (
allowedIps): exact IPv4 or IPv6 addresses, no CIDR ranges. - Optional path/method gate (
allowedPathPrefixes,allowedMethods). - Optional act-as-user impersonation (
actAsUserId, a user id set at creation; theX-Act-As-Userheader is ignored). - An owner: the admin who created it. The token follows that account by id, so renaming the owner keeps it working, and disabling or deleting the owner stops it.
- Minting by another service token: an
adminservice token can mint tokens. They belong to that token’s owner and can never exceed the minting token. See Tokens minted by a service token.
Integration tokens. Dashboards and other read-only integrations use a service token
with the single grant integrations:read. It works only on
GET /api/v1/integrations/homepage, /watch-history and /upcoming, only while the admin
who created it is still an admin, and it cannot act as a user. Admin sessions and admin
tokens cannot read watch history or upcoming episodes. Upgrading converts existing
admin tokens that were limited to those endpoints (GET only) into integration tokens.
Manage service tokens:
| Method | Endpoint | Auth required |
|---|---|---|
GET | /api/v1/service-tokens | admin grant |
POST | /api/v1/service-tokens | admin grant |
DELETE | /api/v1/service-tokens/{id} | admin grant |
For the full walkthrough (device flow code samples, playback grants, and library automation), see API & tokens.