#!/usr/bin/env python3 """Configure Authentik as the OIDC provider for Immich OAuth. Requires both Authentik and Immich to already be deployed. Ensures the OAuth2 provider/application exist in Authentik, then configures Immich OAuth settings via its REST API. No pip dependencies -- stdlib only. Usage: python3 utils/setup_immich_sso.py \ --authentik-domain auth.example.com \ --authentik-token \ --immich-domain photos.example.com \ --immich-admin-email admin@example.com \ --immich-admin-pass """ import argparse import json import os import sys import urllib.error import urllib.request # Allow importing authentik_client from the same directory sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from authentik_client import AuthentikClient def parse_args(): p = argparse.ArgumentParser( description="Set up Authentik OIDC integration for Immich OAuth" ) p.add_argument("--authentik-domain", required=True, help="Authentik domain (e.g. auth.example.com)") p.add_argument("--authentik-token", required=True, help="Authentik admin API token") p.add_argument("--immich-domain", required=True, help="Immich domain (e.g. photos.example.com)") p.add_argument("--immich-admin-email", required=True, help="Immich admin account email") p.add_argument("--immich-admin-pass", required=True, help="Immich admin account password") p.add_argument("--immich-admin-name", default="Admin", help="Immich admin display name (default: Admin)") p.add_argument("--client-id", default="immich", help="OAuth2 client ID (default: immich)") p.add_argument("--app-slug", default="immich", help="Authentik application slug (default: immich)") p.add_argument("--test-user", default="testuser", help="Test user username (default: testuser)") p.add_argument("--test-pass", default="testpass123", help="Test user password (default: testpass123)") p.add_argument("--test-email", default="testuser@test.example.com", help="Test user email") p.add_argument("--no-test-user", action="store_true", help="Skip test user creation") return p.parse_args() def immich_request(base_url, method, path, data=None, token=None): """Make an HTTP request to the Immich API. Returns parsed JSON.""" url = f"{base_url}{path}" body = json.dumps(data).encode() if data is not None else None req = urllib.request.Request(url, data=body, method=method) req.add_header("Content-Type", "application/json") if token: req.add_header("Authorization", f"Bearer {token}") try: with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read() return json.loads(raw) if raw else {} except urllib.error.HTTPError as e: raw = e.read().decode(errors="replace") raise RuntimeError( f"Immich API {method} {path} returned {e.code}: {raw}" ) from e def setup_immich_admin(immich_url, email, password, name): """Create admin account (skip if exists) and login. Returns access token.""" # Try creating admin account print(" Creating Immich admin account ...") try: immich_request(immich_url, "POST", "/api/auth/admin-sign-up", { "email": email, "password": password, "name": name, }) print(" Admin account created") except RuntimeError as e: if "admin" in str(e).lower() or "exists" in str(e).lower(): print(" Admin account already exists, continuing") else: print(f" Admin signup response: {e}") print(" Continuing with login attempt...") # Login print(" Logging in as Immich admin ...") login_resp = immich_request(immich_url, "POST", "/api/auth/login", { "email": email, "password": password, }) access_token = login_resp.get("accessToken") if not access_token: raise RuntimeError(f"Could not login to Immich: {login_resp}") print(f" Logged in (token: {access_token[:10]}...)") return access_token def configure_immich_oauth(immich_url, access_token, issuer_url, client_id, client_secret, app_slug): """Fetch current Immich system config and merge OAuth settings.""" print(" Fetching current Immich system config ...") config = immich_request( immich_url, "GET", "/api/system-config", token=access_token ) # Merge OAuth settings oauth = config.get("oauth", {}) oauth.update({ "enabled": True, "issuerUrl": issuer_url, "clientId": client_id, "clientSecret": client_secret, "scope": "openid email profile", "autoRegister": True, "autoLaunch": False, "buttonText": "Login with Authentik", "tokenEndpointAuthMethod": "client_secret_post", "timeout": 30000, "mobileOverrideEnabled": False, "mobileRedirectUri": "", "signingAlgorithm": "RS256", "storageLabelClaim": "preferred_username", "storageQuotaClaim": "immich_quota", "defaultStorageQuota": 0, "profileSigningAlgorithm": "none", "roleClaim": "immich_role", }) config["oauth"] = oauth print(" Updating OAuth settings ...") put_resp = immich_request( immich_url, "PUT", "/api/system-config", data=config, token=access_token, ) oauth_enabled = put_resp.get("oauth", {}).get("enabled", False) if oauth_enabled: print(" OAuth enabled successfully") else: print(f" WARNING: Could not confirm OAuth enabled") print(f" Response oauth.enabled: {oauth_enabled}") def main(): args = parse_args() ak_url = f"https://{args.authentik_domain}" immich_url = f"https://{args.immich_domain}" client = AuthentikClient(ak_url, args.authentik_token) # Step 1: Resolve UUIDs print("=== Resolving Authentik UUIDs ===") uuids = client.resolve_uuids() print() # Step 2: Create OAuth2 provider print(f"=== Ensuring OAuth2 provider '{args.client_id}' ===") redirect_uris = [ {"matching_mode": "strict", "url": f"{immich_url}/auth/login"}, {"matching_mode": "strict", "url": f"{immich_url}/user-settings"}, {"matching_mode": "strict", "url": "app.immich:///oauth-callback"}, ] provider_pk, client_secret = client.ensure_oauth2_provider( name=args.client_id, client_id=args.client_id, redirect_uris=redirect_uris, uuids=uuids, ) print(f" Client ID: {args.client_id}") print(f" Client secret: {client_secret}") print() # Step 3: Create application print(f"=== Ensuring application '{args.app_slug}' ===") client.ensure_application( name="Immich", slug=args.app_slug, provider_pk=provider_pk, launch_url=immich_url, ) print() # Step 4: Optionally create test user app_password = None if not args.no_test_user: print(f"=== Creating test user '{args.test_user}' ===") user_pk = client.ensure_test_user( args.test_user, args.test_email, args.test_pass ) app_password = client.ensure_app_password( user_pk, f"{args.test_user}-app-password" ) print() # Step 5: Configure Immich OAuth via API (app must already be deployed) print("=== Configuring Immich OAuth via API ===") print(f" Checking Immich is reachable at {immich_url} ...") try: immich_request(immich_url, "GET", "/api/server/ping") except Exception as e: print(f" ERROR: Immich app not reachable at {immich_url}") print(f" The app must be deployed before running this script.") print(f" Details: {e}") sys.exit(1) print(" Immich is reachable") access_token = setup_immich_admin( immich_url, args.immich_admin_email, args.immich_admin_pass, args.immich_admin_name, ) issuer_url = ( f"{ak_url}/application/o/{args.app_slug}" f"/.well-known/openid-configuration" ) configure_immich_oauth( immich_url, access_token, issuer_url, args.client_id, client_secret, args.app_slug, ) print() # Summary print("=== Setup complete ===") print() print(f" Authentik domain: {args.authentik_domain}") print(f" Immich domain: {args.immich_domain}") print(f" Client ID: {args.client_id}") print(f" App slug: {args.app_slug}") print(f" Issuer URL: {issuer_url}") if app_password: print(f" Test user: {args.test_user}") print(f" App password: {app_password[:10]}...") print() print("No redeploy needed -- Immich OAuth config takes effect immediately.") print(f" Open https://{args.immich_domain} and click 'Login with Authentik'") if __name__ == "__main__": main()