#!/usr/bin/env python3 """Configure Authentik as the OIDC provider for CryptPad SSO. Requires both Authentik and CryptPad to already be deployed via abra. Ensures the OAuth2 provider/application exist in Authentik, updates the CryptPad abra .env file, and inserts the client secret as a Docker secret. No pip dependencies -- stdlib only. Usage: python3 utils/setup_cryptpad_sso.py \ --authentik-domain auth.example.com \ --authentik-token \ --cryptpad-domain pad.example.com """ import argparse import os import re import subprocess import sys # 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 CryptPad SSO" ) 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("--cryptpad-domain", required=True, help="CryptPad domain (e.g. pad.example.com)") p.add_argument("--client-id", default="cryptpad", help="OAuth2 client ID (default: cryptpad)") p.add_argument("--app-slug", default="cryptpad", help="Authentik application slug (default: cryptpad)") 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 find_env_file(cryptpad_domain): """Locate the abra .env file for the CryptPad instance. Searches ~/.abra/servers/*/cryptpad_domain.env """ abra_servers = os.path.expanduser("~/.abra/servers") if not os.path.isdir(abra_servers): return None for server in os.listdir(abra_servers): candidate = os.path.join(abra_servers, server, f"{cryptpad_domain}.env") if os.path.isfile(candidate): return candidate return None def update_env_file(env_path, oidc_url, client_id): """Update CryptPad env file with SSO settings.""" with open(env_path, "r") as f: content = f.read() replacements = { "COMPOSE_FILE": "compose.yml:compose.sso.yml", "SSO_ENABLED": "true", "SSO_PROVIDER_NAME": "Authentik", "SSO_OIDC_URL": oidc_url, "SSO_CLIENT_ID": client_id, "SSO_CLIENT_SECRET_VERSION": "v1", } for key, value in replacements.items(): pattern = rf"^{re.escape(key)}=.*$" replacement = f"{key}={value}" if re.search(pattern, content, re.MULTILINE): content = re.sub(pattern, replacement, content, flags=re.MULTILINE) else: content = content.rstrip("\n") + f"\n{replacement}\n" print(f" Set {key}={value}") # Remove old SSO_CLIENT_SECRET env var if present (now a Docker secret) if re.search(r"^SSO_CLIENT_SECRET=", content, re.MULTILINE): content = re.sub( r"^SSO_CLIENT_SECRET=.*\n?", "", content, flags=re.MULTILINE ) print(" Removed old SSO_CLIENT_SECRET env var (now a Docker secret)") with open(env_path, "w") as f: f.write(content) def insert_docker_secret(cryptpad_domain, client_secret): """Insert client secret as a Docker secret via abra.""" cmd = [ "abra", "app", "secret", "insert", cryptpad_domain, "sso_client_s", "v1", client_secret, "--chaos", "--no-input", ] print(f" Running: abra app secret insert {cryptpad_domain} sso_client_s v1 ***") # abra secret insert needs a TTY wrapper wrapped = f'script -qefc "{" ".join(cmd)}" /dev/null' result = subprocess.run( wrapped, shell=True, capture_output=True, text=True ) if result.returncode != 0: # Secret may already exist -- check stderr output = result.stdout + result.stderr if "already exists" in output.lower() or "secret already" in output.lower(): print(" Secret already exists, skipping insert") else: print(f" WARNING: secret insert returned {result.returncode}") if output.strip(): print(f" Output: {output.strip()}") else: print(" Inserted sso_client_s v1") def main(): args = parse_args() ak_url = f"https://{args.authentik_domain}" cpad_url = f"https://{args.cryptpad_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"{cpad_url}/ssoauth"}, ] 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="CryptPad", slug=args.app_slug, provider_pk=provider_pk, launch_url=cpad_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: Update CryptPad env file print("=== Updating CryptPad env file ===") oidc_url = f"{ak_url}/application/o/{args.app_slug}" env_path = find_env_file(args.cryptpad_domain) if env_path is None: print(f" ERROR: CryptPad abra app not found for {args.cryptpad_domain}") print(" The app must exist before running this script.") print(" Create it first with: abra app new cryptpad ...") sys.exit(1) print(f" Found env file: {env_path}") update_env_file(env_path, oidc_url, args.client_id) print() # Step 6: Insert Docker secret print("=== Inserting Docker secret ===") insert_docker_secret(args.cryptpad_domain, client_secret) # Summary print() print("=== Setup complete ===") print() print(f" Authentik domain: {args.authentik_domain}") print(f" CryptPad domain: {args.cryptpad_domain}") print(f" Client ID: {args.client_id}") print(f" App slug: {args.app_slug}") print(f" OIDC URL: {oidc_url}") if app_password: print(f" Test user: {args.test_user}") print(f" App password: {app_password[:10]}...") print() print("Redeploy CryptPad to activate SSO:") print(f" abra app deploy {args.cryptpad_domain} --chaos --force --no-input") if __name__ == "__main__": main()