Skip to main content

Authentication

Authentication is disabled by default, so the web UI binds to 127.0.0.1 unless you decide otherwise. To listen beyond localhost, either enable authentication, name the address yourself (--host, IMMICH_MEMORIES_SERVER__HOST, or a server.host that is not the 0.0.0.0 wildcard), or set server.allow_unauthenticated_lan: true. A bare server.host: 0.0.0.0 in a config file does not count; the loader ignores it with a warning. In Docker the image passes --host 0.0.0.0, so the port mapping is the real boundary: the shipped compose file publishes 127.0.0.1:8080:8080.

Anyone who reaches the port can use the app and, through it, your Immich library. They cannot read the saved API key, and they cannot point it at a server of their own: the UI only sends the key to the URL it was saved for, and a new URL needs its key typed in. The UI is single-user, single-replica: workflow state lives in the process, so a shared secret or volume does not make replicas safe.

Login pageLogin page

Basic auth​

Two variables, in a .env beside docker-compose.yml or in the service's environment::

IMMICH_MEMORIES_AUTH_USERNAME=admin
IMMICH_MEMORIES_AUTH_PASSWORD=your-password-here

Set both; one alone is ignored, and setting both turns auth on with provider: basic. From a shell each line needs export in front of it. The YAML form:

advanced:
auth:
enabled: true
provider: basic
username: admin
password: ${MY_SECRET_PASSWORD} # password, client_secret, issuer_url and client_id expand ${VAR}

immich-memories --config PATH ui uses PATH for every decision, authentication included: bind address, auth, the Immich connection and the settings pages all read the one file, and a reload reloads that same file.

OIDC / SSO​

Any OpenID Connect provider, with PKCE, discovered from issuer_url/.well-known/openid-configuration. Docker images ship authlib; a pip install needs immich-memories[auth].

advanced:
auth:
enabled: true
provider: oidc
issuer_url: https://your-idp.example.com
client_id: immich-memories
client_secret: ${OIDC_CLIENT_SECRET} # empty for public clients
public_url: https://memories.example.com # the URL users reach you on
allowed_emails: [me@example.com]
allowed_domains: [example.com] # exact: does not admit sub.example.com
auto_launch: false # true: straight to the IdP, no login page

public_url pins the redirect_uri sent to the IdP, so it must match what you registered there, and it is what makes callback-origin validation possible. Register the callback URL https://your-host/auth/callback and the logout URL https://your-host/logout. Auth0: a Regular Web Application. Keycloak: a client with client authentication enabled. Authelia: a client with pkce_challenge_method: S256, scopes openid, email, profile, grant type authorization_code. The rest of the auth keys are in the config reference.

An expired sign-in or a reused callback returns you to the sign-in page with "Sign-in expired. Try again." Click the SSO button to start a fresh sign-in. This page waits for your click even with auto_launch: true.

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, Immich API key included. Fine for a personal tenant; wrong for a shared realm or an Auth0 tenant with social sign-ups. Set allowed_emails or allowed_domains: either list admits, matching ignores case, and once either is set the token must carry both email and the boolean email_verified: true. Configure your provider to verify addresses and include that claim: a matching address the IdP has not verified is refused, because on a tenant with open sign-ups anyone can type one. A refused account gets a "Not authorised" page, not a redirect loop.

Trusted header (reverse proxy)​

For a proxy that authenticates (Traefik + Authelia, nginx + oauth2-proxy) and forwards the user in a header.

advanced:
auth:
enabled: true
provider: header
user_header: Remote-User # default
email_header: Remote-Email # default, optional
trusted_proxies:
- 172.20.0.2 # your proxy; prefer a /32 over a whole Docker network
session_ttl_hours: 24

Headers are read only when the immediate peer is in trusted_proxies; from anyone else the request is unauthenticated, and an empty trusted_proxies here fails validation at startup. The app strips nothing, so a proxy that forwards the header from an untrusted source is your problem to fix. Authelia forwards Remote-User and Remote-Email by default.

Behind a reverse proxy with TLS​

Four things, whichever provider you use:

  1. Do not publish 8080 on the LAN once the proxy is up: bind it to localhost, or keep the container on the proxy network only.
  2. List the proxy in auth.trusted_proxies, so its X-Forwarded-For and X-Forwarded-Proto are honoured. Without it every visitor looks like the proxy (five wrong passwords from anyone lock login for everyone for ten minutes) and the OIDC redirect_uri is built as http://. "*" trusts any peer; use it only when the app is unreachable except through the proxy.
  3. Set auth.public_url to the address users type.
  4. Set server.secure_cookies: true so the session cookie carries Secure, but only once every visitor arrives over HTTPS: with it on, a plain http:// origin other than localhost cannot log in.

FORWARDED_ALLOW_IPS in the environment wins over trusted_proxies when set.

Sessions​

A session is a signed cookie and lasts session_ttl_hours (24 by default). The key that signs it is IMMICH_MEMORIES_STORAGE_SECRET, or else ~/.immich-memories/.storage_secret, generated on first start, so logins survive a restart; change it and every session ends. Sign out in the top bar, or /logout, ends the session; OIDC logout also ends the IdP session when the IdP advertises end_session_endpoint. Scripts authenticate to the trigger endpoint with server.trigger_token instead.