Skip to main content

Zitadel vs Casdoor vs Authentik: Open Source IAM in 2026

·PkgPulse Team

Zitadel vs Casdoor vs Authentik: Open Source IAM in 2026

TL;DR

Zitadel is the cloud-native identity server built in Go — designed for SaaS multi-tenancy, B2B scenarios, and Kubernetes-first deployments. Clean admin UI, excellent OIDC implementation, and strong organization (tenant) management. Casdoor is the access control specialist — built on Casbin, it handles complex permission models (RBAC, ABAC, ACL) alongside standard authentication. Best when your use case involves fine-grained authorization, not just authentication. Authentik is the most flexible all-rounder — Python/Go hybrid, works as SSO provider, identity proxy, or LDAP provider, with the richest integration ecosystem. For SaaS B2B multi-tenancy: Zitadel. For complex authorization rules: Casdoor. For replacing enterprise tools (Okta/Auth0 self-hosted): Authentik.

Key Takeaways

  • Zitadel GitHub stars: ~9k — the fastest-growing modern identity server
  • Authentik has 200+ integrations — LDAP, RADIUS, SAML, OIDC, proxy mode covers most enterprise apps
  • Casdoor uses Casbin's policy engine — supports RBAC, ABAC, ACL, RESTful, and URL-matching models
  • All three support OIDC, OAuth 2.0, and SAML 2.0 — standard protocol coverage is solid
  • Zitadel requires the least configuration for a clean OIDC/OAuth setup
  • Authentik's proxy mode can add SSO to apps that don't support it natively (Nginx auth_request)
  • All three ship as Docker containers — deployable in <30 minutes for basic setup

Why Self-Hosted IAM?

Commercial identity providers (Okta, Auth0, Azure AD B2C) cost $3–$8 per monthly active user. For a 10,000 MAU app, that's $30,000–$80,000/year. Self-hosted alternatives reduce that to server costs ($50–$200/month).

Beyond cost, self-hosted IAM means:

  • Data sovereignty — user credentials stay in your infrastructure
  • No vendor lock-in — migrate freely between identity providers
  • Custom flows — registration, login, and MFA flows you fully control
  • Compliance — healthcare, finance, and government require specific data residency

Zitadel: Cloud-Native Identity for SaaS

Zitadel is built for multi-tenant SaaS from the ground up. Every concept (organization, project, application, user) maps to the multi-tenant model. Written in Go, it runs as a single binary backed by CockroachDB or PostgreSQL.

Docker Compose Setup

version: "3.8"

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: zitadel
      POSTGRES_USER: zitadel
      POSTGRES_PASSWORD: zitadel-db-password
    volumes:
      - zitadel-db:/var/lib/postgresql/data

  zitadel:
    image: ghcr.io/zitadel/zitadel:latest
    command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
    environment:
      ZITADEL_DATABASE_POSTGRES_HOST: db
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel-db-password
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_EXTERNALDOMAIN: localhost
      ZITADEL_EXTERNALPORT: 8080
    ports:
      - "8080:8080"
    depends_on:
      - db

volumes:
  zitadel-db:

OIDC Integration (Node.js)

// Zitadel OIDC with openid-client
import { Issuer, generators } from "openid-client";

async function setupZitadelOIDC() {
  const issuer = await Issuer.discover("https://your-zitadel.com");

  const client = new issuer.Client({
    client_id: process.env.ZITADEL_CLIENT_ID!,
    client_secret: process.env.ZITADEL_CLIENT_SECRET!,
    redirect_uris: ["https://yourapp.com/callback"],
    response_types: ["code"],
  });

  return client;
}

// Authorization URL
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);

const authUrl = client.authorizationUrl({
  scope: "openid email profile",
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
});

// Token exchange after callback
const tokenSet = await client.callback(
  "https://yourapp.com/callback",
  { code: callbackCode },
  { code_verifier: codeVerifier }
);

const userinfo = await client.userinfo(tokenSet.access_token!);

Zitadel Management API (Multi-Tenancy)

// Create a new organization (tenant) programmatically
async function createOrganization(name: string, adminEmail: string) {
  const response = await fetch(
    "https://your-zitadel.com/management/v1/orgs",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${serviceAccountToken}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name }),
    }
  );

  const org = await response.json();
  return org.org.id;
}

// Invite a user to an organization
async function inviteUser(orgId: string, email: string) {
  await fetch(
    `https://your-zitadel.com/management/v1/orgs/${orgId}/members`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${serviceAccountToken}`,
        "Content-Type": "application/json",
        "x-zitadel-orgid": orgId,
      },
      body: JSON.stringify({
        userId: email,
        roles: ["ORG_OWNER"],
      }),
    }
  );
}

Zitadel Actions (Custom Logic)

// Zitadel Actions — JavaScript functions that run during auth flows
// Add this via the Admin Console under "Actions"

// Example: Add custom claims to tokens
function setCustomClaims(ctx, api) {
  // ctx contains user info, request details
  const orgId = ctx.org.id;

  // Add org tier from your database (simulated here)
  const tier = lookupTier(orgId); // Your custom function

  api.v1.claims.setClaim("org_tier", tier);
  api.v1.claims.setClaim("org_id", orgId);
}

Casdoor: Access Control Meets Identity

Casdoor combines authentication with Casbin's powerful policy engine. While most identity servers do authentication and basic role-based access control, Casdoor handles complex permission models natively.

Docker Compose Setup

version: "3.8"

services:
  db:
    image: mysql:8-debian
    environment:
      MYSQL_ROOT_PASSWORD: casdoor-root
      MYSQL_DATABASE: casdoor
    volumes:
      - casdoor-db:/var/lib/mysql

  casdoor:
    image: casbin/casdoor-all-in-one:latest
    ports:
      - "8000:8000"
    environment:
      RUNNING_IN_DOCKER: "true"
    depends_on:
      - db

volumes:
  casdoor-db:

OIDC Integration

// Casdoor is OIDC-compliant — standard integration
import { casdoor } from "casdoor-nodejs-sdk";

const casdoorConfig = {
  endpoint: "http://localhost:8000",
  clientId: process.env.CASDOOR_CLIENT_ID!,
  clientSecret: process.env.CASDOOR_CLIENT_SECRET!,
  certificate: process.env.CASDOOR_PUBLIC_CERT!,
  orgName: "my-organization",
  appName: "my-app",
};

const sdk = new casdoor.SDK(casdoorConfig);

// Get authorization URL
const authUrl = sdk.getAuthLink(
  "https://yourapp.com/callback",
  "state-random-string",
  "code",
  "openid email profile"
);

// Exchange code for token
const token = await sdk.getOAuthToken(
  "code-from-callback",
  "https://yourapp.com/callback"
);

const userInfo = await sdk.parseJwtToken(token.access_token);

Casbin Permission Policies

// Casdoor uses Casbin's policy engine — define complex authorization rules

// RBAC policy (model.conf)
// [request_definition]
// r = sub, obj, act
// [policy_definition]
// p = sub, obj, act
// [role_definition]
// g = _, _
// [matchers]
// m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

// Policies (stored in Casdoor database, manageable via UI):
// p, admin, /api/*, *          # Admins can do anything on /api/*
// p, user, /api/read/*, GET   # Users can GET from /api/read/*
// g, alice, admin              # alice is an admin
// g, bob, user                 # bob is a user

// Policy enforcement in your app
import { Enforcer } from "casbin";
import { CasdoorAdapter } from "casbin-casdoor-adapter";

const enforcer = await Enforcer.newEnforcer(
  "path/to/model.conf",
  new CasdoorAdapter(casdoorConfig)
);

// Check permission
async function canAccess(userId: string, resource: string, action: string) {
  const allowed = await enforcer.enforce(userId, resource, action);
  return allowed;
}

// Usage
if (await canAccess("alice", "/api/users/123", "DELETE")) {
  // Proceed with deletion
}

ABAC (Attribute-Based Access Control)

// Casdoor ABAC — policy based on attributes, not just roles

// Model: r = sub_attr, dom, obj_attr, act
// p = sub_attr.department == "engineering" && obj_attr.sensitivity == "low", *, *, read

const enforcer = await Enforcer.newEnforcer("abac-model.conf", "abac-policy.csv");

// Pass attribute objects directly
const user = { id: "user_123", department: "engineering", clearance: "level2" };
const document = { id: "doc_456", sensitivity: "low", owner: "user_789" };

const canRead = await enforcer.enforce(user, "company-portal", document, "read");
const canEdit = await enforcer.enforce(user, "company-portal", document, "write");

Authentik: The Enterprise-Grade All-Rounder

Authentik is written in Python (backend) and TypeScript (frontend) and provides the most comprehensive integration surface: OIDC provider, SAML provider, LDAP server, RADIUS server, and application proxy. It can SSO-enable any app, even ones without native SSO support.

Docker Compose Setup

version: "3.8"

services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: authentik-db-pass
      POSTGRES_USER: authentik
      POSTGRES_DB: authentik
    volumes:
      - authentik-db:/var/lib/postgresql/data

  redis:
    image: docker.io/library/redis:alpine

  server:
    image: ghcr.io/goauthentik/server:latest
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik-db-pass
      AUTHENTIK_SECRET_KEY: "your-secret-key-min-50-chars-long"
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
    ports:
      - "9000:9000"
      - "9443:9443"
    depends_on:
      - postgresql
      - redis

  worker:
    image: ghcr.io/goauthentik/server:latest
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: authentik-db-pass
      AUTHENTIK_SECRET_KEY: "your-secret-key-min-50-chars-long"
    depends_on:
      - postgresql
      - redis

volumes:
  authentik-db:

OIDC Provider Integration

// Authentik as OIDC provider
import { generators, Issuer } from "openid-client";

const issuer = await Issuer.discover("https://auth.yourcompany.com/application/o/your-app/");

const client = new issuer.Client({
  client_id: process.env.AUTHENTIK_CLIENT_ID!,
  client_secret: process.env.AUTHENTIK_CLIENT_SECRET!,
  redirect_uris: ["https://yourapp.com/callback"],
  response_types: ["code"],
});

Authentik Proxy Mode (SSO for Legacy Apps)

# nginx.conf — Authentik outpost proxy mode
# Adds SSO to any app without changing the app itself

upstream authentik {
  server authentik-outpost:9000;
}

server {
  listen 443 ssl;
  server_name legacy-app.company.com;

  location /outpost.goauthentik.io {
    proxy_pass http://authentik;
    proxy_set_header Host $host;
    proxy_set_header X-Original-URL $scheme://$host$request_uri;
    add_header Set-Cookie $auth_cookie;
    auth_request_set $auth_cookie $upstream_http_set_cookie;
  }

  # All requests go through auth check
  auth_request /outpost.goauthentik.io/auth/nginx;
  error_page 401 = @goauthentik_proxy_signin;
  auth_request_set $auth_header $upstream_http_authorization;
  proxy_set_header Authorization $auth_header;

  location @goauthentik_proxy_signin {
    internal;
    add_header Set-Cookie $auth_cookie;
    return 302 /outpost.goauthentik.io/start?rd=$request_uri;
  }

  location / {
    proxy_pass http://legacy-app-backend;
  }
}

Flows and Stages (Custom Auth Flows)

# Authentik policy — Python expressions for custom logic (in Admin UI)

# Example: Only allow users from specific email domains
def akpolicy_domain_check(request: PolicyRequest) -> PolicyResult:
    user = request.user
    allowed_domains = ["company.com", "partner.org"]
    email_domain = user.email.split("@")[-1]

    if email_domain in allowed_domains:
        return PolicyResult(passing=True)
    else:
        return PolicyResult(
            passing=False,
            messages=[f"Email domain {email_domain} is not allowed"]
        )

Feature Comparison

FeatureZitadelCasdoorAuthentik
Primary languageGoGoPython + Go
OIDC provider
SAML 2.0
LDAP serverPartial✅ Full
RADIUS server
Proxy mode (SSO without code)
Multi-tenancy / Organizations✅ NativePartial
Fine-grained RBAC/ABACBasic roles✅ CasbinRBAC
Custom login flowsActions (JS)Partial✅ Full flow engine
Social login (GitHub, Google)✅ 30+ providers
User management UI
Self-service password reset
MFA (TOTP, WebAuthn)
API-first management✅ gRPC + REST✅ REST✅ REST
Kubernetes operator
GitHub stars9k11k16k
Docker image size~50 MB~150 MB~500 MB
Memory usage (idle)~100 MB~100 MB~300 MB

When to Use Each

Choose Zitadel if:

  • You're building a SaaS product with multi-tenant organization management (B2B auth)
  • Kubernetes-native deployment with Go's operational characteristics matter
  • You want the cleanest OIDC implementation with the smallest config surface
  • Your auth flows are standard (login, registration, MFA) without exotic customization

Choose Casdoor if:

  • Your authorization requirements are complex (RBAC + ABAC + domain-level policies)
  • You already use or want Casbin's policy model
  • You need unified auth + authorization in one system
  • Audit trails for who accessed what under which policy are required

Choose Authentik if:

  • You need to SSO-enable legacy apps without modifying them (proxy mode)
  • Your stack includes LDAP-dependent tools (GitLab, Jenkins, older enterprise apps)
  • You want the richest integration ecosystem (200+ provider support)
  • You're replacing Okta/Azure AD for a private company SSO layer

Methodology

Data sourced from GitHub repositories (star counts as of February 2026), official documentation, Docker Hub image sizes, and community benchmarks from self-hosted server forums (r/selfhosted, awesome-selfhosted). Memory usage measurements from community-reported Docker stats. Feature availability verified from official documentation and GitHub issue trackers.


Related: Logto vs Ory vs Keycloak for alternative open-source identity providers, or WorkOS vs Stytch vs FusionAuth for enterprise SSO solutions.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.