Skip to content

Authentication & Access

narratorr-requests separates authentication (who you are) from authorization (who may request). Authentication is pluggable; authorization is the in-app approval queue. Enable either or both sign-in methods.

On by default (LOCAL_AUTH=true). Email is the login, and starts out as the contact address — the contact email is separately editable afterwards in the account modal (click your username) without affecting how you sign in. Passwords are hashed with scrypt and compared in constant time. For OIDC users the contact email is backfilled from provider claims on login; a manually saved address wins over later claims, and clearing it re-enables the backfill.

  • Users Create account / Sign in from the login page.
  • Passwords must be 8–200 characters.
  • A wrong login returns a generic “Invalid email or password” (no account enumeration on the login path).

Set LOCAL_AUTH=false to run pure-OIDC.

List provider ids in OIDC_PROVIDERS (comma-separated), then configure each id <ID> with OIDC_<UPPERCASE_ID>_* variables. Ids must be lowercase letters/digits/underscores (so pocketid, not pocket-id) — an invalid or duplicate id fails at startup. Each provider gets its own login button and a callback at /api/auth/oidc/<id>/callback. Plex (via a bridge), Authelia, Authentik, Keycloak, Pocket-ID, and Google are all just provider instances — there’s no special-casing.

Terminal window
OIDC_PROVIDERS=plex,authelia
# Plex, via a plex-oidc-bridge:
OIDC_PLEX_ISSUER=https://plex-auth.example.com
OIDC_PLEX_CLIENT_ID=narratorr-requests
OIDC_PLEX_CLIENT_SECRET=
OIDC_PLEX_REDIRECT_URI=https://requests.example.com/api/auth/oidc/plex/callback
OIDC_PLEX_LABEL=Plex
# Authelia (Authentik / Keycloak / Pocket-ID / Google are the same shape):
OIDC_AUTHELIA_ISSUER=https://auth.example.com
OIDC_AUTHELIA_CLIENT_ID=narratorr-requests
OIDC_AUTHELIA_CLIENT_SECRET=
OIDC_AUTHELIA_REDIRECT_URI=https://requests.example.com/api/auth/oidc/authelia/callback
OIDC_AUTHELIA_LABEL=Authelia

Per provider, ISSUER / CLIENT_ID / REDIRECT_URI are required; CLIENT_SECRET is optional (omit it for a public PKCE client). LABEL, SCOPE (default openid profile email), and the SUBJECT_CLAIM / USERNAME_CLAIM / EMAIL_CLAIM overrides are optional. The flow uses PKCE (S256) with single-use state and nonce.

A valid sign-in is not enough to request — every account has a status:

StatusWhat the user can do
pendingSigned in, but sees an “Awaiting approval” screen — can’t search or request.
activeFull access: search, request, track.
rejectedSees an “Access not approved” screen. Durable — survives re-login.

A new user lands pending. An admin moves them to active (or rejected) on the Users page. The gate is enforced both client-side and server-side (a pending account’s API calls get 403 ACCOUNT_PENDING). Admins are always treated as active.

  1. First-user-auto-admin (default). The first real identity to sign in — in any method — becomes admin + active automatically. (The AUTH_BYPASS dev-admin sentinel doesn’t count, so flipping a smoke instance to real auth still lets your first real user claim admin.)

  2. Pin it instead with BOOTSTRAP_ADMIN. Set BOOTSTRAP_ADMIN=<provider>:<subjectOrUsername> (e.g. authelia:todd, plex:myplexuser, or local:you@example.com) before that identity’s first sign-in to make exactly that identity admin + active and leave everyone else pending. This disables first-user-auto-admin. The grant applies when the account is created — setting it later won’t promote an existing account (or demote an existing admin); fix those on the Users page.

Set TRUSTED_PROXIES (same variable name as narratorr) so the real client IP is used for the login rate-limiter — set it to true (trust all), a hop count, or a CIDR/IP list (e.g. your Docker proxy network). Auth endpoints are rate-limited by client IP and attempted email to slow brute-force attempts.

AUTH_BYPASS=1 seeds a dev admin and makes every request that admin — no login at all. It’s refused outright in production (no escape hatch), and refused on a non-loopback bind unless you also set ALLOW_INSECURE_AUTH_BYPASS=1. Use it to kick the tires locally; never on anything reachable by others.