Skip to main content

Authentication

By default, the web UI has no authentication — and because of that, it binds to 127.0.0.1 unless you decide otherwise. To listen beyond localhost you either enable authentication, name the address yourself (--host, IMMICH_MEMORIES_SERVER__HOST, or a server.host in the config file that is not the 0.0.0.0 wildcard), or set server.allow_unauthenticated_lan: true — a name that spells out what it allows. (The Docker image passes --host 0.0.0.0 explicitly — inside a container that is the only useful bind — so which port the compose file publishes is the real boundary. The shipped docker-compose.yml publishes 127.0.0.1:8080:8080; enable auth before you widen it.)

One line in a config file does not count: a bare server.host: 0.0.0.0. Older versions wrote it into every config they saved, chosen or not, so it says nothing about intent and the loader now ignores it with a warning. If your LAN bind came from that line, set server.allow_unauthenticated_lan: true (or pass --host) — after reading the next paragraph.

When authentication is disabled and the server listens beyond localhost, anyone who can reach the port can use the app — and, through it, your Immich library. The UI is single-user, single-replica because active workflow state lives in the process. A shared storage secret or volume does not make multiple replicas safe.

Login pageLogin page

Quick start (basic auth)

Set two env vars:

IMMICH_MEMORIES_AUTH_USERNAME=admin
IMMICH_MEMORIES_AUTH_PASSWORD=your-password-here

Done. The UI now requires login.

Providers

Basic auth

Username and password stored in config. Credentials are compared with constant-time comparison (no timing side-channels).

Env vars (quickest way):

IMMICH_MEMORIES_AUTH_USERNAME=admin
IMMICH_MEMORIES_AUTH_PASSWORD=your-password-here

Setting both vars automatically enables auth and sets the provider to basic. Set both — one alone is ignored. The shortcut is applied only when the app loads the default config path; with immich-memories --config PATH (or a scheduler started with an explicit config file) use the full form instead: IMMICH_MEMORIES_AUTH__ENABLED=true, IMMICH_MEMORIES_AUTH__PROVIDER=basic, IMMICH_MEMORIES_AUTH__USERNAME, IMMICH_MEMORIES_AUTH__PASSWORD.

YAML config (under advanced.auth):

advanced:
auth:
enabled: true
provider: basic
username: admin
password: your-password-here
session_ttl_hours: 24 # default

The password, client_secret, issuer_url and client_id fields support ${ENV_VAR} expansion, so you can avoid hardcoding secrets:

advanced:
auth:
enabled: true
provider: basic
username: admin
password: ${MY_SECRET_PASSWORD}

OIDC / SSO

Connect to any OpenID Connect provider. Uses PKCE automatically: no extra config needed.

YAML config:

advanced:
auth:
enabled: true
provider: oidc
issuer_url: https://your-idp.example.com
client_id: immich-memories
client_secret: ${OIDC_CLIENT_SECRET} # or leave empty for public clients
public_url: https://memories.example.com # the URL users reach you on
allowed_domains: [example.com] # who may sign in; empty = anyone the IdP accepts
scope: openid email profile # default
session_ttl_hours: 24 # default
auto_launch: false # if true, redirect directly to IdP (skips the login page)
button_text: "Sign in with SSO" # text shown on the login button

The issuer_url is used to auto-discover the OIDC configuration from /.well-known/openid-configuration.

Set public_url to the address users actually reach the app on. It pins the redirect_uri sent to the IdP — which must match what you registered there — instead of letting it be derived from whatever host the request resolves to, and it is what allows the callback origin to be validated.

Your IdP decides who you are, not whether you may enter

With no allow-list, anyone your IdP authenticates becomes a full admin of this app — including the Immich API key. That is fine for a personal tenant, and wrong for a shared Keycloak/Authentik realm or an Auth0 tenant with social sign-ups, where "anyone the IdP authenticates" means anyone with a Google account.

Set at least one of these:

advanced:
auth:
allowed_emails:
- me@example.com
allowed_domains:
- example.com # exact: does NOT admit sub.example.com

Either list admits, so you can allow a whole domain plus a named outside contractor. Matching ignores case. Once either list is set, an account whose token carries no email claim is refused — otherwise an IdP that omits the claim would be a way around the list.

Someone who signs in successfully but is not on the list gets a "Not authorised" page naming the account they used, not a redirect back to the login page — their IdP session is still valid, so a redirect would simply loop.

authlib is required for OIDC. Install with:

pip install 'immich-memories[auth]'

Or with Docker, use the image tag that includes auth extras (the default image includes it).

Auth0 example

Two files. The compose file passes the secret in as an environment variable:

# docker-compose.yml
services:
immich-memories:
image: ghcr.io/sam-dumont/immich-video-memory-generator:latest
environment:
- IMMICH_URL=https://photos.example.com
- IMMICH_API_KEY=${IMMICH_API_KEY}
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
# ...

The app config references it (do not paste the services: block into config.yaml — unknown top-level keys make the config fail to load):

# ~/.immich-memories/config.yaml
advanced:
auth:
enabled: true
provider: oidc
issuer_url: https://YOUR_DOMAIN.auth0.com
client_id: YOUR_CLIENT_ID
client_secret: ${OIDC_CLIENT_SECRET}

In Auth0, create a Regular Web Application. Set the callback URL to https://your-host/auth/callback and logout URL to https://your-host/logout.

Authelia example

advanced:
auth:
enabled: true
provider: oidc
issuer_url: https://auth.example.com
client_id: immich-memories
client_secret: ${OIDC_CLIENT_SECRET}

In Authelia's configuration.yml, register a client:

identity_providers:
oidc:
clients:
- client_id: immich-memories
client_secret: '$argon2id$...' # hashed with authelia crypto hash generate
redirect_uris:
- https://memories.example.com/auth/callback
scopes:
- openid
- email
- profile
grant_types:
- authorization_code
response_types:
- code
pkce_challenge_method: S256

Keycloak example

advanced:
auth:
enabled: true
provider: oidc
issuer_url: https://keycloak.example.com/realms/YOUR_REALM
client_id: immich-memories
client_secret: ${OIDC_CLIENT_SECRET}

In Keycloak, create a client with client authentication enabled. Set the redirect URI to https://memories.example.com/auth/callback.


Trusted header (reverse proxy)

For setups where a reverse proxy (Traefik + Authelia, nginx + oauth2-proxy, etc.) handles authentication and forwards the verified user via request headers.

How it works: The middleware reads the Remote-User header (configurable) and creates a session automatically. Requests from untrusted IPs have the headers stripped before they reach the app.

trusted_proxies is required

With provider: header and an empty trusted_proxies, the config fails validation and the app refuses to start (trusted_proxies must be non-empty when provider is 'header'). Without the check, any client could forge the headers and gain access. Set this to your proxy's IP or CIDR range.

YAML config:

advanced:
auth:
enabled: true
provider: header
user_header: Remote-User # header containing the username
email_header: Remote-Email # header containing the email (optional)
trusted_proxies:
- 172.20.0.0/16 # your proxy's IP/CIDR
- 192.168.1.10
session_ttl_hours: 24

Traefik + Authelia example

services:
immich-memories:
image: ghcr.io/sam-dumont/immich-video-memory-generator:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.memories.rule=Host(`memories.example.com`)"
- "traefik.http.routers.memories.middlewares=authelia@docker"
networks:
- proxy

traefik:
image: traefik:v3
# ... your Traefik config ...

authelia:
image: authelia/authelia:latest
# ... your Authelia config ...

In your config YAML, set trusted_proxies to Traefik's container IP (prefer a single /32 over a whole Docker network CIDR — on a shared proxy network any container in that range could send Remote-User). Authelia forwards Remote-User and Remote-Email by default, which match the defaults.


Session

  • Sessions last 24 hours by default (session_ttl_hours). Range: 1-720 hours.
  • The same value expires session state on disk: abandoned session files are swept at startup and every 15 minutes thereafter, so an exposed instance doesn't accumulate one file per drive-by request.
  • Logout via the sidebar button or by navigating to /logout.
  • OIDC logout redirects to the IdP's end_session_endpoint when available (the IdP terminates the SSO session too). Falls back to a local-only logout if the IdP doesn't advertise that endpoint.
  • A session is for browsers. Scripts and workflows authenticate to the trigger endpoint with server.trigger_token instead — no cookie, no login page.
Behind a reverse proxy: bind locally, name the proxy, mark the cookie Secure

Four things to set when the UI sits behind Traefik, Caddy or nginx with TLS:

  1. Don't publish 8080 on the LAN once the proxy is up. Bind it to localhost (127.0.0.1:8080:8080 in compose) or keep the container on the proxy network only.
  2. List the proxy in auth.trusted_proxies. The app passes it to uvicorn as forwarded_allow_ips, so X-Forwarded-For / X-Forwarded-Proto from that address are honored. Without it every visitor looks like the proxy's IP — five wrong passwords from anyone lock the whole household out of login for 10 minutes — and the OIDC redirect_uri is built as http://… instead of https://…, which most identity providers reject. "*" trusts any peer; only use it when the app is unreachable except through the proxy.
  3. Set auth.public_url to the address users actually type, e.g. https://memories.example.com. It pins the OIDC redirect_uri to that origin rather than whatever the request resolves to, and it is what makes callback-origin validation possible — without it there is nothing trustworthy to compare an incoming Host header against, so no origin check is performed and the IdP's registered redirect_uri is the only control.
  4. Set server.secure_cookies: true so the session cookie carries the Secure flag and browsers never send it over plain HTTP. Only do this once every visitor arrives over HTTPS — with it on, a plain http:// origin (other than localhost) can't log in at all.
# ~/.immich-memories/config.yaml
advanced:
server:
secure_cookies: true
auth:
enabled: true
provider: basic # or oidc
username: admin
password: ${MY_SECRET_PASSWORD}
public_url: https://memories.example.com
trusted_proxies:
- 172.20.0.2 # your proxy container's IP or CIDR
# docker-compose.yml
services:
immich-memories:
ports:
- "127.0.0.1:8080:8080" # or drop `ports:` entirely and expose via the proxy network

The FORWARDED_ALLOW_IPS environment variable still works and, when set, wins over trusted_proxies. With provider: header the forwarded headers are deliberately not applied: the app has to keep seeing the proxy's own address to trust Remote-User, and the login rate limiter and OIDC redirect don't apply to that provider anyway. Leave FORWARDED_ALLOW_IPS unset in that case.

The login rate limiter reads X-Forwarded-For the same way — only when the immediate peer is in auth.trusted_proxies. Without that, every login behind a proxy shares one bucket and five wrong passwords from anyone lock out everyone for ten minutes; with it, an untrusted caller still cannot pick its own bucket by setting the header itself.

When running without a reverse proxy (direct access on localhost), none of this applies.


Config reference

All fields under advanced.auth with their defaults:

FieldDefaultDescription
enabledfalseEnable authentication
providerbasicProvider: basic, oidc, or header
session_ttl_hours24How long a session lasts (1–720)
public_url""The URL users reach this app on, e.g. https://memories.example.com. Pins the OIDC redirect_uri and enables callback-origin validation
allowed_emails[]OIDC: addresses permitted to sign in. Empty allows any account the IdP authenticates
allowed_domains[]OIDC: email domains permitted to sign in. Exact match — example.com does not admit sub.example.com
username""Basic auth: username
password""Basic auth: password (supports ${ENV_VAR})
issuer_url""OIDC: IdP base URL (autodiscovery via /.well-known/openid-configuration; supports ${ENV_VAR})
client_id""OIDC: client ID (supports ${ENV_VAR})
client_secret""OIDC: client secret (empty for public clients; supports ${ENV_VAR})
scopeopenid email profileOIDC: requested scopes
auto_launchfalseOIDC: redirect directly to IdP (skip the login page)
button_textSign in with SSOOIDC: login button label
user_headerRemote-UserHeader: header name for the username
email_headerRemote-EmailHeader: header name for the email
trusted_proxies[]IPs/CIDRs of your reverse proxy. Header provider: allowed to set auth headers. Basic/OIDC: their X-Forwarded-For / X-Forwarded-Proto are honored (uvicorn forwarded_allow_ips)