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).
254 lines
9.0 KiB
Python
Executable File
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()
|