Zitadel vs Casdoor vs Authentik: Open Source IAM in 2026
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
| Feature | Zitadel | Casdoor | Authentik |
|---|---|---|---|
| Primary language | Go | Go | Python + Go |
| OIDC provider | ✅ | ✅ | ✅ |
| SAML 2.0 | ✅ | ✅ | ✅ |
| LDAP server | Partial | ✅ | ✅ Full |
| RADIUS server | ❌ | ❌ | ✅ |
| Proxy mode (SSO without code) | ❌ | ❌ | ✅ |
| Multi-tenancy / Organizations | ✅ Native | ✅ | Partial |
| Fine-grained RBAC/ABAC | Basic roles | ✅ Casbin | RBAC |
| Custom login flows | Actions (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 stars | 9k | 11k | 16k |
| 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.