ZID is a lightweight, self-hosted identity provider (IdP) built for homelabs, small services, and internal tools.
It implements a CAS-style authentication flow: users log in to ZID, receive a one-time ticket, and your application verifies that ticket via a simple /verify call to obtain the user's identity. No complex token management on your side — just redirect, verify, done.
ZID also supports optional OpenID Connect / OAuth 2.0 for applications that need standard OIDC integration.
- Single binary deployment
- CAS-style one-time ticket authentication
- Optional OpenID Connect / OAuth 2.0 (Authorization Code + PKCE, Client Credentials)
- Browser login flow with SSO cookies (login once, access multiple services)
- JSON API for programmatic authentication
- Telegram login support
- PostgreSQL, SQLite, and Redis storage backends
- Docker deployment
ZID focuses on simplicity:
- minimal setup
- predictable authentication flow
- small deployment footprint
- easy integration with small services
Most identity providers (Keycloak, Authentik, Authelia) are powerful but come with significant operational complexity — large deployments, steep learning curves, and resource overhead.
ZID takes a different approach: minimal setup, simple integration, and just enough features for smaller deployments. If you run a homelab, a handful of internal services, or a small project that needs shared authentication — ZID gets out of your way.
sequenceDiagram
participant User as User Browser
participant App as Application
participant ZID as ZID Server
User->>App: Access protected resource
App->>User: 302 → ZID /?return_to=APP_URL
User->>ZID: Open login form
User->>ZID: POST / (username + password)
ZID->>User: 302 → APP_URL?ticket=TICKET
User->>App: Follow redirect with ticket
App->>ZID: POST /verify {ticket, service}
ZID->>App: {user_id, username, session_id}
App->>User: Authenticated
- Redirect the user to:
GET http://zid-host:5555/?return_to=https://yourapp.com/callback
- User logs in.
- ZID redirects to:
https://yourapp.com/callback?ticket=<TICKET>
- Your backend calls:
POST http://zid-host:5555/verifywith{ "ticket": "...", "service": "https://yourapp.com/callback" }
- You receive
{ user_id, username, session_id }and create your own session/cookie.
sequenceDiagram
participant Client
participant ZID as ZID Server
Client->>ZID: POST /login {username, password}
ZID->>Client: {ticket}
Client->>ZID: POST /verify {ticket, service}
ZID->>Client: {user_id, username, session_id}
- Client calls
POST /login(JSON) withoutreturn_to. - ZID returns
{ "ticket": "..." }. - You call
/verifyand get the user identity.
graph TB
Browser[User Browser]
App[Your Application]
ZID[ZID Server]
PG[(PostgreSQL)]
Redis[(Redis<br/><i>optional</i>)]
OIDC[OIDC Client App]
Browser -->|login / SSO cookie| ZID
App -->|/verify ticket| ZID
OIDC -->|/oauth/authorize<br/>/oauth/token| ZID
ZID --> PG
ZID -.-> Redis
App -->|redirect to /login| Browser
docker compose up -d
# ZID will be available at http://localhost:5555# Download the latest release for your platform
chmod +x zid
./zidRequires PostgreSQL. See Configuration for environment variables.
Health check:
curl -s http://localhost:5555/healthZID supports SQLite as a storage backend — ideal for development, homelabs, and single-node deployments where PostgreSQL is overkill.
To run ZID with SQLite (no external database required):
export SESSION_STORAGE=sqlite
export TICKET_STORAGE=sqlite
export CREDENTIALS_STORAGE=sqlite
export SQLITE_PATH=zid.db
./zidZID creates the database file and all tables automatically on first start.
SQLite uses WAL mode and foreign keys by default. For production deployments with higher concurrency, PostgreSQL is recommended.
ZID uses an SSO cookie (zid_sso) to recognize returning users and offer Continue without re-entering credentials.
Modern browsers reject cookies with the Secure attribute over plain HTTP. Therefore:
- in production ZID should run behind HTTPS (cookie must be
Secure) - locally (
http://localhost:5555) you need to disableSecure, otherwise SSO won't persist
Controlled via ZID_COOKIE_SECURE:
auto(default): ZID detects HTTPS from headers (X-Forwarded-Proto=https/Forwarded: proto=https)true/1: always setSecurefalse/0: never setSecure(useful for local HTTP development)
Recommendations:
- prod:
ZID_COOKIE_SECURE=auto(ortrue) + HTTPS + proxy must forwardX-Forwarded-Proto=https - local http:
ZID_COOKIE_SECURE=false
| Method | Endpoint | Consumer | Description |
|---|---|---|---|
| GET | / |
Browser | HTML login form (supports return_to) |
| POST | / |
Browser | Login form submit (application/x-www-form-urlencoded) |
| POST | /continue |
Browser | Continue as current user (SSO) |
| GET | /register |
Browser | HTML registration form |
| POST | /register |
Browser | Registration submit (application/x-www-form-urlencoded) |
| POST | /login |
API | JSON login → ticket (+optional redirect_url) |
| POST | /login/telegram |
API | Telegram login JSON → ticket (+optional redirect_url) |
| POST | /verify |
Backend | One-time ticket verification → user info |
| POST | /logout |
Backend/API | Session deletion (by session_id) |
| GET | /health |
Ops | Health check |
| Method | Endpoint | Description |
|---|---|---|
| GET | /.well-known/openid-configuration |
Discovery (server metadata) |
| GET | /oauth/authorize |
Authorization endpoint (code flow) |
| POST | /oauth/token |
Token endpoint (exchange code for tokens, client_credentials) |
| GET | /oauth/userinfo |
UserInfo (Bearer access_token via Authorization header) |
| GET | /oauth/jwks |
JWKS (public keys for JWT verification) |
Open in browser:
http://localhost:5555/?return_to=https://yourapp.com/callback
return_to— your application's URL (where ZID will redirect the user back). It must pass trusted domain validation (seeTRUSTED_DOMAINSbelow).
The form submits to POST / (handled by the browser).
ZID redirects the user to:
https://yourapp.com/callback?ticket=<uuid>
The ticket is one-time and short-lived.
Request:
curl -X POST http://localhost:5555/verify \
-H "Content-Type: application/json" \
-d '{
"ticket":"<TICKET_FROM_QUERY>",
"service":"https://yourapp.com/callback"
}'Response (example):
{
"success": true,
"user_id": "176f8257-4bec-4350-99bf-e023186fd04a",
"username": "alice",
"session_id": "b2a2d2d6-8c8f-4f65-bfb0-3d2bb4c1c6d9"
}Next steps:
- Create your own session (cookie/JWT) and
- Redirect the user to the protected area of your application.
If return_to is not set, there will be no redirect.
Open: http://localhost:5555/
After login, ZID returns an HTML success page displaying the ticket.
Useful for manual debugging and simple integrations.
Request:
curl -X POST http://localhost:5555/login \
-H "Content-Type: application/json" \
-d '{
"username":"alice",
"password":"secret123"
}'Response:
{
"ticket": "4b53f154-3747-463c-9a57-6e856edf4f3a"
}Then verify the ticket via /verify as described above.
Request:
curl -X POST http://localhost:5555/login \
-H "Content-Type: application/json" \
-d '{
"username":"alice",
"password":"secret123",
"return_to":"https://yourapp.com/callback"
}'Response:
{
"ticket": "7ee2015f-6112-4db7-adac-d46d03ece91f",
"redirect_url": "https://yourapp.com/callback?ticket=7ee2015f-6112-4db7-adac-d46d03ece91f"
}
redirect_urlis optional and only returned whenreturn_tois provided and non-empty.
Open: http://localhost:5555/register
curl -X POST http://localhost:5555/register \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'username=alice&password=secret123&password_confirm=secret123'Supports login via Telegram Widget (on the login page) and the /login/telegram endpoint.
Required env vars:
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_BOT_USERNAME=your_bot_username
TELEGRAM_AUTO_REGISTER=trueDetails and examples: docs/TELEGRAM_LOGIN.md
ZID supports OIDC/OAuth 2.0 (Authorization Code + PKCE, Client Credentials).
OIDC is enabled by default. If the clients file or JWT keys are not configured, the server starts without OIDC (endpoints return 503). To disable explicitly: OIDC_ENABLED=false.
-
Generate RSA keys:
openssl genrsa -out oidc_jwt_private.pem 2048 openssl rsa -in oidc_jwt_private.pem -pubout -out oidc_jwt_public.pem
-
Copy the clients file:
cp oidc_clients.example.yaml oidc_clients.yaml
-
Set environment variables:
OIDC_ENABLED=true OIDC_ISSUER=http://localhost:5555 OIDC_CLIENTS_FILE=oidc_clients.yaml OIDC_JWT_PRIVATE_KEY=oidc_jwt_private.pem OIDC_JWT_PUBLIC_KEY=oidc_jwt_public.pem
sequenceDiagram
participant User as User Browser
participant Client as OIDC Client
participant ZID as ZID Server
Client->>User: Redirect → /oauth/authorize
User->>ZID: Authenticate (login or SSO cookie)
ZID->>User: 302 → callback?code=CODE
User->>Client: Follow redirect with authorization code
Client->>ZID: POST /oauth/token {code, client_id, client_secret}
ZID->>Client: {access_token, id_token, refresh_token}
Details and curl examples: docs/OIDC_TESTING.md
- One-time: a second
/verifyfor the same ticket will be rejected. - TTL ~ 5 minutes (tickets expire quickly).
- Tickets are bound to a
serviceURL: you must pass the same service to/verifythat was used when the ticket was issued.
ZID validates return_to against a list of trusted domains.
Configured via TRUSTED_DOMAINS:
TRUSTED_DOMAINS=localhost,127.0.0.1,*.local.dev,*.myapp.com,myapp.example.comIn production, make sure to add your application domains.
POST /loginreturns an error (HTTP status depends on the handler; HTML returns a 401 Unauthorized page).
If return_to fails validation, login will be rejected. Check TRUSTED_DOMAINS.
/verify will not return user info.
# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=zid
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
# Redis
REDIS_URL=redis://127.0.0.1:6380
# Server
SERVER_HOST=0.0.0.0
SERVER_PORT=5555
# return_to allowlist
TRUSTED_DOMAINS=localhost,127.0.0.1,*.local.dev,*.local,*.lan
# Telegram (optional)
TELEGRAM_BOT_TOKEN=
TELEGRAM_BOT_USERNAME=
TELEGRAM_AUTO_REGISTER=true
# Cookie / SSO
ZID_COOKIE_SECURE=auto # auto (default) / true / false
# Storage backend (optional)
SESSION_STORAGE=postgres # postgres (default), redis, or sqlite
TICKET_STORAGE=postgres # postgres (default), redis, or sqlite
CREDENTIALS_STORAGE=postgres # postgres (default), redis, or sqlite
# SQLite (when using sqlite storage)
SQLITE_PATH=zid.db # path to SQLite database file
# OIDC/OAuth 2.0 (optional; enabled by default, starts without OIDC if config/keys are missing)
# OIDC_ENABLED=true
# OIDC_ISSUER=http://localhost:5555
# OIDC_CLIENTS_FILE=oidc_clients.yaml
# OIDC_JWT_PRIVATE_KEY=oidc_jwt_private.pem
# OIDC_JWT_PUBLIC_KEY=oidc_jwt_public.pemSESSION_STORAGE/TICKET_STORAGE/CREDENTIALS_STORAGEpostgres(default): PostgreSQL storageredis: Redis with built-in TTL supportsqlite: SQLite file-based storage (great for simple deployments)
# start
docker compose up -d
# logs
docker compose logs -f zid-app
# stop
docker compose downDefault services:
- App:
http://localhost:5555 - PostgreSQL:
localhost:5432 - Redis:
localhost:6380
E2E test (register → login → verify):
./scripts/test.shE2E test OIDC (discovery, client_credentials, jwks):
./scripts/test-oidc.shdocs/TELEGRAM_LOGIN.md— Telegram Login integrationdocs/OIDC_TESTING.md— OIDC/OAuth 2.0 testing guidedocs/FREEBSD_SETUP.md— FreeBSD installation and setup