- Remove all ZITADEL_FIRSTINSTANCE_* environment variables - Fixes migration error: duplicate key constraint violation - Root cause: Bug in Zitadel v2.63.7 FirstInstance migration - Workaround: Complete initial setup via web UI - Upstream issue: https://github.com/zitadel/zitadel/issues/8791 Changes: - Clean up obsolete documentation (OIDC_AUTOMATION.md, SETUP_GUIDE.md, COLLABORA_SETUP.md) - Add PROJECT_REFERENCE.md for essential configuration info - Add force recreate functionality with clean database volumes - Update bootstrap instructions for web UI setup - Document one-time manual setup requirement for OIDC automation Zitadel now deploys successfully and is accessible at: https://zitadel.test.vrije.cloud 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
169 lines
5.5 KiB
Python
169 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Create OIDC application in Zitadel using the Management API.
|
|
|
|
This script automates the creation of OIDC applications for services like Nextcloud.
|
|
It uses Zitadel's REST API with service account authentication.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import requests
|
|
from typing import Dict, Optional
|
|
|
|
|
|
class ZitadelOIDCManager:
|
|
"""Manage OIDC applications in Zitadel."""
|
|
|
|
def __init__(self, domain: str, pat_token: str):
|
|
"""Initialize the OIDC manager."""
|
|
self.domain = domain
|
|
self.base_url = f"https://{domain}"
|
|
self.headers = {
|
|
"Authorization": f"Bearer {pat_token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
def create_project(self, project_name: str) -> Optional[str]:
|
|
"""Create a project."""
|
|
url = f"{self.base_url}/management/v1/projects"
|
|
payload = {
|
|
"name": project_name
|
|
}
|
|
response = requests.post(url, headers=self.headers, json=payload)
|
|
|
|
if response.status_code in [200, 201]:
|
|
return response.json().get("id")
|
|
return None
|
|
|
|
def get_or_create_project(self, project_name: str = "SSO Applications") -> Optional[str]:
|
|
"""Get existing project or create new one."""
|
|
# Try to list projects
|
|
url = f"{self.base_url}/management/v1/projects/_search"
|
|
response = requests.post(url, headers=self.headers, json={})
|
|
|
|
if response.status_code == 200:
|
|
projects = response.json().get("result", [])
|
|
for project in projects:
|
|
if project.get("name") == project_name:
|
|
return project["id"]
|
|
# If no matching project, use first one if exists
|
|
if projects:
|
|
return projects[0]["id"]
|
|
|
|
# No project found, try to create one
|
|
project_id = self.create_project(project_name)
|
|
return project_id
|
|
|
|
def check_app_exists(self, project_id: str, app_name: str) -> Optional[Dict]:
|
|
"""Check if an OIDC app already exists."""
|
|
url = f"{self.base_url}/management/v1/projects/{project_id}/apps/_search"
|
|
response = requests.post(url, headers=self.headers, json={})
|
|
|
|
if response.status_code == 200:
|
|
apps = response.json().get("result", [])
|
|
for app in apps:
|
|
if app.get("name") == app_name:
|
|
return app
|
|
return None
|
|
|
|
def create_oidc_app(
|
|
self,
|
|
project_id: str,
|
|
app_name: str,
|
|
redirect_uris: list,
|
|
post_logout_redirect_uris: list = None,
|
|
) -> Dict:
|
|
"""Create an OIDC application."""
|
|
url = f"{self.base_url}/management/v1/projects/{project_id}/apps/oidc"
|
|
|
|
payload = {
|
|
"name": app_name,
|
|
"redirectUris": redirect_uris,
|
|
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
|
|
"grantTypes": [
|
|
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
|
"OIDC_GRANT_TYPE_REFRESH_TOKEN",
|
|
],
|
|
"appType": "OIDC_APP_TYPE_WEB",
|
|
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
|
|
"postLogoutRedirectUris": post_logout_redirect_uris or [],
|
|
"version": "OIDC_VERSION_1_0",
|
|
"devMode": False,
|
|
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER",
|
|
"accessTokenRoleAssertion": True,
|
|
"idTokenRoleAssertion": True,
|
|
"idTokenUserinfoAssertion": True,
|
|
"clockSkew": "0s",
|
|
}
|
|
|
|
response = requests.post(url, headers=self.headers, json=payload)
|
|
|
|
if response.status_code in [200, 201]:
|
|
return response.json()
|
|
else:
|
|
raise Exception(
|
|
f"Failed to create OIDC app: {response.status_code} - {response.text}"
|
|
)
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
if len(sys.argv) < 5:
|
|
print("Usage: create_oidc_app.py <domain> <pat_token> <app_name> <redirect_uri>")
|
|
sys.exit(1)
|
|
|
|
domain = sys.argv[1]
|
|
pat_token = sys.argv[2]
|
|
app_name = sys.argv[3]
|
|
redirect_uri = sys.argv[4]
|
|
|
|
try:
|
|
manager = ZitadelOIDCManager(domain, pat_token)
|
|
|
|
# Get or create project
|
|
project_id = manager.get_or_create_project("SSO Applications")
|
|
if not project_id:
|
|
print("Error: Failed to get or create project", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Check if app already exists
|
|
existing_app = manager.check_app_exists(project_id, app_name)
|
|
if existing_app:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": "exists",
|
|
"app_id": existing_app.get("id"),
|
|
"message": f"App '{app_name}' already exists",
|
|
}
|
|
)
|
|
)
|
|
sys.exit(0)
|
|
|
|
# Create new app
|
|
result = manager.create_oidc_app(
|
|
project_id=project_id,
|
|
app_name=app_name,
|
|
redirect_uris=[redirect_uri],
|
|
post_logout_redirect_uris=[redirect_uri.rsplit("/", 1)[0] + "/"],
|
|
)
|
|
|
|
# Extract client credentials
|
|
output = {
|
|
"status": "created",
|
|
"app_id": result.get("appId"),
|
|
"client_id": result.get("clientId"),
|
|
"client_secret": result.get("clientSecret"),
|
|
"redirect_uri": redirect_uri,
|
|
}
|
|
|
|
print(json.dumps(output))
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|