Files
recipe-maintainer/utils/setup_immich_sso.py
autonomic-bot f283a371bb recipe-maintainer: public snapshot (secrets + deployment plans removed, single commit)
Sanitized single-commit public mirror of recipe-maintainer.
- Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders.
- Removed plans/ and planned-updates/ (deployment-planning docs) so no client/
  deployment domains appear in the public repo.
- All other secret stores were already gitignored.
- docs.coopcloud.tech retained as a submodule (public upstream).
2026-06-16 20:18:24 +00:00

254 lines
9.0 KiB
Python
Executable File

#!/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 <admin-api-token> \
--immich-domain photos.example.com \
--immich-admin-email admin@example.com \
--immich-admin-pass <password>
"""
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()