Skip to content

Authentication

John R. D'Orazio edited this page Feb 18, 2026 · 4 revisions

Authentication

OntoKit uses Zitadel as its identity provider, implementing OpenID Connect (OIDC) for authentication.

Architecture

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Browser   │────>│  Zitadel    │────>│ OntoKit    │
│   (Web App) │<────│  (OIDC)     │<────│    API      │
└─────────────┘     └─────────────┘     └─────────────┘
                           │
                    JWT Access Token

Zitadel Setup

1. Start Zitadel

Zitadel is included in the Docker Compose setup:

docker compose up -d

Wait for Zitadel to be ready:

# Check health endpoint
curl http://localhost:8080/debug/ready

2. Access Zitadel Console

Open http://localhost:8080/ui/console in your browser.

Default Admin Credentials:

  • Username: admin@ontokit.localhost
  • Password: Admin123!

3. Automatic Setup (Recommended)

Run the setup script to create the OIDC application and configure credentials:

./scripts/setup-zitadel.sh --update-env

The script will:

  1. Wait for Zitadel to be ready
  2. Create an "OntoKit" project (or reuse existing)
  3. Create an "OntoKit Web" OIDC application (or reuse existing)
  4. Update both ontokit-api/.env and ontokit-web/.env.local with credentials
  5. Prompt to recreate the API container (if running in Docker)

Script flags:

Flag Description
--update-env Update .env files with credentials
--docker-init Start Docker stack if not running, then configure
--force-secrets Regenerate client secrets (invalidates existing sessions)

The script is idempotent — safe to run multiple times. It preserves existing sessions by reusing secrets when the client ID matches.

4. Restart Services

After the script updates the .env files, restart the services to pick up the new credentials:

# Full Docker mode — recreate API and worker containers
docker compose up -d --force-recreate api worker

# Hybrid mode — restart your local uvicorn process and the Next.js dev server

Manual Setup

If the automatic setup doesn't work, follow these steps:

Create a Project

  1. Go to Projects > Create Project
  2. Name: OntoKit
  3. Important: Leave "Assert Roles on Authentication" and "Check User Grants" disabled (these are advanced features that require additional configuration)

Create Web Application

  1. In the OntoKit project, go to Applications > New
  2. Select Web Application
  3. Name: OntoKit Web
  4. Configure:
    • Redirect URIs: http://localhost:3000/api/auth/callback/zitadel
    • Post Logout URIs: http://localhost:3000
    • Auth Method: BASIC (client secret via HTTP Basic Auth)
    • Dev Mode: Enabled (allows http:// redirect URIs)
  5. Note the Client ID and Client Secret

JWT Token Validation

The API validates JWT tokens from Zitadel using the JWKS endpoint.

How It Works

  1. Client sends request with Authorization: Bearer <token> header
  2. API fetches JWKS from Zitadel (/.well-known/openid-configuration)
  3. API validates the token signature using the public key
  4. API extracts user information from token claims

Token Claims

Claim Description
sub User ID (unique identifier)
email User's email address
name User's display name
preferred_username Username

API Authentication

Protected Endpoints

Endpoints that require authentication use the RequiredUser dependency:

from ontokit.core.auth import RequiredUser

@router.post("/projects")
async def create_project(user: RequiredUser):
    # user is guaranteed to be authenticated
    print(f"Creating project for user: {user.id}")

Optional Authentication

For endpoints that work differently for authenticated vs anonymous users:

from ontokit.core.auth import OptionalUser

@router.get("/projects")
async def list_projects(user: OptionalUser):
    if user:
        # Show user's private projects too
        pass
    else:
        # Only show public projects
        pass

Making Authenticated Requests

From the Frontend

The frontend (Next.js) uses NextAuth.js to manage authentication:

import { useSession } from "next-auth/react";

function MyComponent() {
  const { data: session } = useSession();

  const response = await fetch("/api/v1/projects", {
    headers: {
      Authorization: `Bearer ${session?.accessToken}`,
    },
  });
}

From CLI/Scripts

Use the Device Authorization Grant flow:

# 1. Request device code
curl -X POST http://localhost:8000/api/v1/auth/device/code \
  -H "Content-Type: application/json" \
  -d '{"client_id": "your-client-id"}'

# Response includes device_code, user_code, and verification_uri

# 2. User visits verification_uri and enters user_code

# 3. Poll for token
curl -X POST http://localhost:8000/api/v1/auth/device/token \
  -H "Content-Type: application/json" \
  -d '{"client_id": "your-client-id", "device_code": "..."}'

User Roles and Permissions

Project Roles

Role Permissions
owner Full control, can delete project, transfer ownership
admin Manage members, update settings, cannot delete
editor Edit ontology content
viewer Read-only access

Authorization Logic

  • Public projects: Anyone can view
  • Private projects: Only members can view
  • Create project: Any authenticated user
  • Update project: Owner or admin
  • Delete project: Owner only
  • Manage members: Owner or admin

Creating Test Users

Via Zitadel Console

  1. Go to Users > Create User
  2. Fill in the user details
  3. Set a password

Via API (with admin token)

# Get admin PAT from the Zitadel container
docker cp ontokit-zitadel:/zitadel-data/admin.pat /tmp/admin.pat
PAT=$(cat /tmp/admin.pat)

# Create user
curl -X POST http://localhost:8080/management/v1/users/human \
  -H "Authorization: Bearer $PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "userName": "testuser",
    "profile": {
      "firstName": "Test",
      "lastName": "User"
    },
    "email": {
      "email": "test@example.com",
      "isEmailVerified": true
    },
    "password": {
      "password": "TestPassword123!"
    }
  }'

Troubleshooting

"App not found" or "invalid_request"

The OIDC application hasn't been created in Zitadel. Run the setup script:

./scripts/setup-zitadel.sh --update-env

Then recreate the API container: docker compose up -d --force-recreate api worker

"Unknown error occurred" on Login

This usually means "User Grant Required". The OntoKit project has role checking enabled but users don't have grants. Fix by disabling role check:

# Get admin PAT
docker cp ontokit-zitadel:/zitadel-data/admin.pat /tmp/admin.pat
PAT=$(cat /tmp/admin.pat)

# Get project ID
PROJECT_ID=$(curl -s -H "Authorization: Bearer $PAT" \
  -X POST "http://localhost:8080/management/v1/projects/_search" \
  -H "Content-Type: application/json" \
  -d '{"queries":[{"nameQuery":{"name":"OntoKit","method":"TEXT_QUERY_METHOD_EQUALS"}}]}' \
  | python3 -c "import sys, json; print(json.load(sys.stdin)['result'][0]['id'])")

# Disable role check
curl -X PUT "http://localhost:8080/management/v1/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $PAT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OntoKit",
    "projectRoleAssertion": false,
    "projectRoleCheck": false
  }'

"Token validation failed"

  • Check that ZITADEL_ISSUER matches the Zitadel URL
  • Verify the token hasn't expired
  • Ensure Zitadel is running: curl http://localhost:8080/debug/ready

"Unable to find appropriate key"

The JWKS cache may be stale. Restart the API server.

CORS Errors

Ensure CORS_ORIGINS includes your frontend URL:

CORS_ORIGINS=["http://localhost:3000"]

Zitadel Login Loop

Clear cookies and try again. Check that redirect URIs match exactly.

After Database Reset

If you reset the database with docker compose down -v, you need to reconfigure Zitadel:

  1. Start services: docker compose up -d
  2. Re-run the setup script: ./scripts/setup-zitadel.sh --update-env
  3. Recreate API/worker containers: docker compose up -d --force-recreate api worker

Next Steps

Clone this wiki locally