Developer Documentation

Everything you need to implement ZenoAuth

From 5-minute quick start to advanced deployment configurations. Comprehensive guides, API references, and real-world examples.

Quick Start

Get ZenoAuth running in under 5 minutes with Docker or manual installation.

Multi-Factor Auth

Configure TOTP-based MFA with backup codes for enhanced account security.

SSO Providers

Connect external identity providers like Google, Microsoft, GitHub, and Okta.

SCIM Provisioning

Configure SCIM v2 user and group provisioning for enterprise identity management.

Key Management

Manage Ed25519 signing keys with rotation, JWKS distribution, and revocation.

Admin Interface

Modern Next.js admin dashboard for user management, analytics, and audit logging.

Privileged Identity Management

JIT access elevation, approval workflows, break-glass emergency access, and auto-expiry.

Verifiable Credentials

Issue SD-JWT credentials via OID4VCI, verify wallet presentations via OID4VP, manage trusted issuers.

API Reference

Complete REST API documentation with OAuth 2.0 and OpenID Connect endpoints.

Groups & Permissions

Manage groups, roles, and permissions for organization-wide access control.

Dynamic Client Registration

Self-service OAuth client registration with programmatic lifecycle management.

Rich Authorization Requests

Fine-grained authorization with structured permission details beyond scopes.

Quick Start Guide

Option 1: Cloud SaaS (Fastest)

Cloud Setup
# Start your free trial at zenoauth.com 1. Sign up at https://zenoauth.com/signup 2. Create your first organization 3. Configure your OAuth clients # Your endpoints will be: API: https://yourorg.zenoauth.com/api Admin: https://yourorg.zenoauth.com/admin OAuth: https://yourorg.zenoauth.com/oauth

Option 2: Self-Hosted (Docker)

Self-Hosted Setup
# Download from customer portal after license purchase # Contact [email protected] for access # Environment setup export ZENOAUTH__DATABASE__URL="postgres://user:pass@localhost:5432/zenoauth" export ZENOAUTH__SERVER__BASE_URL="http://localhost:3051" export ZENOAUTH__LICENSE_KEY="your-license-key" # Start with Docker Compose docker-compose up -d # Services available: API Server: http://localhost:3051 Admin UI: http://localhost:3050

First Steps After Installation

  1. Access Admin Interface: Navigate to http://localhost:3050
  2. Create Admin User: Register your first admin user via the UI
  3. Register OAuth Client: Create your first OAuth client application
  4. Test Authentication: Test login via /auth/login endpoint
  5. Configure SCIM: Set up SCIM provisioning for your identity provider

API Reference

Authentication Endpoints

Core Auth APIs
# User Registration POST /auth/register Content-Type: application/json { "email": "[email protected]", "password": "secure_password", "first_name": "John", "last_name": "Doe" } # User Login POST /auth/login Content-Type: application/json { "email": "[email protected]", "password": "secure_password" } # Response { "access_token": "eyJ...", "refresh_token": "rt_...", "expires_in": 3600, "token_type": "Bearer" }

OAuth 2.0 Endpoints

OAuth 2.0 APIs
# Authorization Endpoint GET /oauth/authorize? response_type=code& client_id=your_client_id& redirect_uri=https://yourapp.com/callback& scope=openid profile email& state=random_state # Token Endpoint POST /oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=auth_code_from_callback& client_id=your_client_id& client_secret=your_client_secret& redirect_uri=https://yourapp.com/callback # Token Response { "access_token": "eyJ...", "id_token": "eyJ...", "refresh_token": "rt_...", "expires_in": 3600, "token_type": "Bearer", "scope": "openid profile email" }

Pushed Authorization Requests (PAR)

PAR (RFC 9126) enhances OAuth security by allowing clients to push authorization parameters directly to the server via a secure back-channel, eliminating URL-based parameter exposure and tampering risks.

PAR Flow (Two-Step Process)
# Step 1: Push Authorization Request POST /oauth/par Authorization: Basic base64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded response_type=code& redirect_uri=https://yourapp.com/callback& scope=openid profile email& state=random_state& code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM& code_challenge_method=S256 # PAR Response (60-second lifetime) { "request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c", "expires_in": 60 } # Step 2: Authorization Request (simplified) GET /oauth/authorize? client_id=your_client_id& request_uri=urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c # Benefits: • Enhanced Security - Parameters never in browser URL • Prevents Tampering - Server-side parameter storage • No URL Limits - Supports large payloads • Single-Use - request_uri works only once

Dynamic Client Registration (DCR)

DCR (RFC 7591) enables self-service OAuth client registration. Clients can register themselves at runtime without manual admin intervention, receiving management tokens for future updates.

DCR - Self-Service Client Registration
# Step 1: Register New Client (No Auth Required) POST /oauth/register Content-Type: application/json { "client_name": "My Application", "redirect_uris": ["https://myapp.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "client_secret_basic", "scope": "openid profile email" } # Registration Response { "client_id": "dyn_abc123...", "client_secret": "secret_xyz...", "registration_access_token": "rat_token123...", "registration_client_uri": "https://auth.example.com/oauth/register/dyn_abc123", "client_id_issued_at": 1234567890, "client_secret_expires_at": 0 } # Step 2: Read Client Configuration GET /oauth/register/dyn_abc123 Authorization: Bearer rat_token123... # Step 3: Update Client Metadata PUT /oauth/register/dyn_abc123 Authorization: Bearer rat_token123... Content-Type: application/json { "client_name": "Updated App Name", "redirect_uris": ["https://myapp.com/callback", "https://myapp.com/callback2"] } # Step 4: Delete Client Registration DELETE /oauth/register/dyn_abc123 Authorization: Bearer rat_token123... # Benefits: • Self-Service - No admin intervention needed • Secure Management - Token-based client updates • API-Driven - Programmatic client lifecycle • RFC 7591 Compliant - Standard protocol

Rich Authorization Requests (RAR)

RAR (RFC 9396) provides fine-grained authorization beyond simple scopes. Request structured permissions for specific actions on specific resources with detailed authorization_details.

RAR - Fine-Grained Authorization
# Authorization with Structured Details POST /oauth/par Authorization: Basic base64(client_id:client_secret) Content-Type: application/x-www-form-urlencoded response_type=code& redirect_uri=https://myapp.com/callback& scope=openid& authorization_details=[ { "type": "payment_initiation", "actions": ["initiate", "status", "cancel"], "locations": ["https://bank.example.com/payments"], "instructedAmount": { "currency": "EUR", "amount": "123.50" } }, { "type": "account_information", "actions": ["read_balances", "read_transactions"], "locations": ["https://bank.example.com/accounts/12345"] } ] # Token Response with Authorization Details { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "authorization_details": [ { "type": "payment_initiation", "actions": ["initiate", "status", "cancel"], "locations": ["https://bank.example.com/payments"], "instructedAmount": { "currency": "EUR", "amount": "123.50" } } ] } # Supported Detail Types: • payment_initiation - Financial transactions • account_information - Account data access • resource_access - API resource permissions • api_access - Fine-grained API scoping # Benefits: • Fine-Grained - Specific actions on specific resources • Structured - JSON objects with type safety • Consent Clarity - Users see exact permissions • RFC 9396 Compliant - Standard protocol

OpenID Connect

OIDC Discovery
# Discovery Endpoint GET /.well-known/openid-configuration # Response (partial) { "issuer": "https://auth.yourdomain.com", "authorization_endpoint": "/oauth/authorize", "token_endpoint": "/oauth/token", "userinfo_endpoint": "/oauth/userinfo", "jwks_uri": "/oauth/jwks", "scopes_supported": [ "openid", "profile", "email" ], "response_types_supported": [ "code", "token", "id_token" ] } # User Info Endpoint GET /oauth/userinfo Authorization: Bearer eyJ... # Response { "sub": "user_id", "email": "[email protected]", "email_verified": true, "name": "John Doe" }

Admin APIs

Administration
# Create OAuth Client POST /admin/clients Authorization: Bearer admin_token Content-Type: application/json { "name": "My App", "redirect_uris": [ "https://myapp.com/callback" ], "allowed_scopes": [ "openid", "profile", "email" ] } # List Users GET /admin/users Authorization: Bearer admin_token # Get Analytics GET /api/v1/analytics Authorization: Bearer admin_token # Get Audit Logs GET /api/v1/audit Authorization: Bearer admin_token

Privileged Identity Management

Zero standing privileges. Users request time-limited elevated access with justification. Approval policies route requests, privileges auto-expire, and every action is audited.

Request Elevated Access

POST /api/v1/pim/elevations
# Request temporary admin access POST /api/v1/pim/elevations Authorization: Bearer eyJ... Content-Type: application/json { "target_role_id": "ef6b1626-254a-...", "justification": "Production debug INC-4521", "ticket_reference": "INC-4521", "requested_duration_minutes": 60 } # Response (202 Accepted) { "id": "9a4952f8-624c-...", "status": "pending", "requester_id": "bbca78c4-...", "target_role_id": "ef6b1626-...", "requested_duration_minutes": 60, "is_break_glass": false }

Approve or Deny

Approval Flow
# Approve an elevation request POST /api/v1/pim/elevations/{id}/approve { "reason": "Verified migration plan" } # If threshold met → role assigned + activated { "status": "active", "activated_at": "2026-03-10T14:30:55Z", "expires_at": "2026-03-10T15:30:55Z" } # Deny a request POST /api/v1/pim/elevations/{id}/deny { "reason": "Rescheduled to maintenance window" } # Revoke an active elevation early POST /api/v1/pim/elevations/{id}/revoke # Renew before expiry POST /api/v1/pim/elevations/{id}/renew { "additional_minutes": 30 }

Approval Policies

POST /api/v1/pim/policies
# Create a policy governing elevation approvals { "name": "Production Database Access", "description": "Requires 2 security team approvals", "target_role_pattern": "admin", "approval_type": "manual", "required_approvers": 2, "max_duration_minutes": 120, "max_renewals": 1, "require_ticket_reference": true, "require_mfa_at_elevation": true, "priority": 10 } # Auto-approval for trusted patterns { "name": "SRE Incident Response", "approval_type": "auto", "max_duration_minutes": 60, "auto_conditions": [ { "incident_linked": true, "max_duration_minutes": 60 } ] } # CRUD GET /api/v1/pim/policies GET /api/v1/pim/policies/{id} PUT /api/v1/pim/policies/{id} DELETE /api/v1/pim/policies/{id}

Break-Glass Emergency Access

Break-Glass Flow
# 1. Create a break-glass policy POST /api/v1/pim/break-glass-policies { "name": "P1 Emergency Response", "granted_role_id": "2d362946-...", "max_duration_minutes": 30, "require_reason": true, "enhanced_audit": true, "require_post_review": true, "review_deadline_hours": 24, "notify_urls": ["https://hooks.slack.com/..."] } # 2. Activate (immediate access, no approval) POST /api/v1/pim/break-glass { "break_glass_policy_id": "policy-uuid", "reason": "P1: payment processing down", "ticket_reference": "INC-9001" } # 3. Release when resolved POST /api/v1/pim/break-glass/{id}/release # 4. Mandatory post-incident review POST /api/v1/pim/break-glass/{id}/review { "review_notes": "Root cause: expired TLS cert", "actions_taken": ["Renewed cert", "Added monitoring"] }

PIM Dashboard & Monitoring

Dashboard & Query APIs
# Dashboard summary GET /api/v1/pim/dashboard { "active_elevations": 3, "pending_requests": 5, "total_today": 12, "avg_approval_time_minutes": 4.2, "active_break_glass": 0, "recent_requests": [...] } # List with filters GET /api/v1/pim/elevations?status=active GET /api/v1/pim/elevations?status=pending # Pending approvals for current user GET /api/v1/pim/approvals # Analytics GET /api/v1/pim/analytics?date_from=2026-03-01 # Elevation audit trail GET /api/v1/pim/audit?elevation_request_id={id}

Elevation Status Lifecycle

pending approved active completed
pending denied
pending cancelled (by requester)
pending expired (no response)
active revoked (admin override)

Key Behaviors

  • Matching policy found automatically by role pattern
  • Multi-approver threshold before activation
  • Background scheduler expires active elevations
  • Role assignment removed on expiry or revocation
  • Break-glass bypasses approval, triggers alerts

Verifiable Credentials

Issue W3C Verifiable Credentials via OID4VCI, verify wallet presentations via OID4VP, and manage credential lifecycle with Bitstring Status Lists. SD-JWT format with selective disclosure.

Credential Issuer Discovery

Well-Known Endpoints
# Issuer metadata (OID4VCI) GET /.well-known/openid-credential-issuer { "credential_issuer": "https://auth.example.com", "credential_endpoint": "/credentials", "credentials_supported": [ { "format": "vc+sd-jwt", "vct": "EmployeeBadge", "claims": { "name": { "display": "Full Name" }, "department": { "display": "Department" } } } ] } # DID Document GET /.well-known/did.json { "id": "did:web:auth.example.com", "verificationMethod": [{ "type": "JsonWebKey2020", "publicKeyJwk": { ... } }] }

Credential Types

POST /api/v1/credential-types
# Define a credential type { "name": "EmployeeBadge", "description": "Employee identity credential", "format": "vc+sd-jwt", "claims_schema": { "name": { "type": "string", "mandatory": true }, "department": { "type": "string" }, "clearance_level": { "type": "string" }, "employee_id": { "type": "string" } }, "validity_days": 365 } # CRUD GET /api/v1/credential-types GET /api/v1/credential-types/{id} PUT /api/v1/credential-types/{id} DELETE /api/v1/credential-types/{id}

OID4VCI Issuance Flow

Issue credentials via the pre-authorized code flow. Supports both admin-initiated and SCIM-triggered issuance.

Pre-Authorized Issuance (Complete Flow)
# Step 1: Create a credential offer (admin or SCIM trigger) POST /api/v1/credential-offers { "user_id": "bbca78c4-...", "credential_type_id": "type-uuid", "claims": { "name": "Jane Smith", "department": "Engineering", "clearance_level": "confidential" } } # Response includes offer URI for wallet { "offer_uri": "openid-credential-offer://...&pre-authorized_code=abc123", "qr_code_data": "..." } # Step 2: Wallet exchanges pre-authorized code for token POST /oauth/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code& pre-authorized_code=abc123 # Step 3: Wallet requests credential with access token POST /credentials Authorization: Bearer wallet_token { "format": "vc+sd-jwt", "vct": "EmployeeBadge" } # Response: signed SD-JWT with selective disclosure { "format": "vc+sd-jwt", "credential": "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOi...~WyJfc2QiLCJuYW1lIiwiSmFuZSBTbWl0aCJd~" } The ~ delimiters separate: issuer-signed JWT | disclosure 1 | disclosure 2 | ... User chooses which disclosures to include when presenting.

OID4VP Verification

Verify Wallet Presentation
# Create a presentation definition POST /api/v1/presentation-definitions { "name": "Employee Verification", "input_descriptors": [{ "id": "employee_badge", "constraints": { "fields": [ { "path": ["$.vct"], "filter": { "const": "EmployeeBadge" } }, { "path": ["$.name"] } ] } }] } # Verify a presentation from a wallet POST /api/v1/presentations/verify { "vp_token": "eyJhbGci...~disclosure1~", "presentation_definition_id": "pd-uuid" }

Trusted Issuers & Revocation

Trust & Status Management
# Register a trusted issuer POST /api/v1/trusted-issuers { "name": "Acme Corp", "did": "did:web:auth.acme.com", "accepted_credential_types": [ "EmployeeBadge" ], "trust_framework": "internal" } # Revoke a credential (flips status bit) POST /api/v1/credentials/{id}/revoke { "reason": "Employee departed" } # Status list (privacy-preserving) GET /api/v1/credentials/status/{list_id} Bitstring Status List: verifiers check a single bit per credential. No PII leaked. # Credential-to-RBAC rules POST /api/v1/credential-rbac-rules { "credential_type": "EmployeeBadge", "claim_path": "clearance_level", "operator": "equals", "value": "secret", "granted_role_id": "classified-access" }

Standards & Compliance

OID4VCI 1.0

Credential Issuance
pre-authorized + auth code

OID4VP 1.0

Verifiable Presentations
wallet authentication

SD-JWT (RFC 9901)

Selective Disclosure
ECDSA P-256 signing

did:web

Decentralized Identifiers
per-org issuer DIDs

Status List

Bitstring revocation
privacy-preserving

HAIP

High Assurance Profile
eIDAS 2.0 ready

Configuration Guide

Configuration Methods

ZenoAuth supports three configuration methods in order of precedence:

1. Environment Variables

Highest priority. Use ZENOAUTH__SECTION__KEY format.

Environment Variables
export ZENOAUTH__SERVER__PORT=3051 export ZENOAUTH__DATABASE__URL="postgres://..." # Ed25519 keys auto-generated

2. Configuration File

TOML format configuration file at zenoauth.toml.

zenoauth.toml
[server] port = 3051 bind_address = "0.0.0.0" [database] url = "postgres://..." max_connections = 20 [oauth] issuer = "https://auth.example.com"

3. Command Line

Lowest priority. Useful for development and testing.

Command Line
./zenoauth \ --server.port 3051 \ --database.url "postgres://..." \ --config zenoauth.toml

Complete Configuration Reference

Complete zenoauth.toml
# Server Configuration [server] bind_address = "0.0.0.0" port = 8080 base_url = "https://auth.yourdomain.com" workers = 0 # 0 = auto-detect CPU cores # Database Configuration [database] url = "postgres://zenoauth:password@localhost:5432/zenoauth" max_connections = 20 min_connections = 5 idle_timeout_seconds = 300 # Security Configuration [security] jwt_secret = "your-super-secret-jwt-key-at-least-32-characters" session_timeout_seconds = 3600 password_min_length = 8 max_login_attempts = 5 lockout_duration_seconds = 900 # OAuth Configuration [oauth] default_token_lifetime_seconds = 3600 refresh_token_lifetime_seconds = 86400 enable_pkce = true require_pkce = false # Rate Limiting [rate_limiting] enabled = true requests_per_minute = 60 burst_size = 10 # Logging Configuration [logging] level = "info" format = "json" correlation_ids = true # SMTP for Email (Optional) [smtp] host = "smtp.gmail.com" port = 587 username = "[email protected]" password = "your-password" from_address = "[email protected]"

Deployment Guide

Docker Deployment

Production Docker
# docker-compose.prod.yml version: '3.8' services: zenoauth: image: zenoauth:latest ports: - "3051:3051" environment: - ZENOAUTH__DATABASE__URL=postgres://zenoauth:pass@postgres:5432/zenoauth - ZENOAUTH__SERVER__BASE_URL=https://auth.yourdomain.com # Ed25519 keys auto-generated on startup depends_on: - postgres restart: unless-stopped postgres: image: postgres:15 environment: - POSTGRES_DB=zenoauth - POSTGRES_USER=zenoauth - POSTGRES_PASSWORD=${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data restart: unless-stopped volumes: postgres_data:

Kubernetes Deployment

Kubernetes Manifest
# zenoauth-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: zenoauth spec: replicas: 3 selector: matchLabels: app: zenoauth template: metadata: labels: app: zenoauth spec: containers: - name: zenoauth image: zenoauth:latest ports: - containerPort: 3051 env: - name: ZENOAUTH__DATABASE__URL valueFrom: secretKeyRef: name: zenoauth-secrets key: database-url - name: ZENOAUTH__SERVER__BASE_URL value: "https://auth.yourdomain.com" # Ed25519 signing keys auto-generated resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "128Mi" cpu: "500m" readinessProbe: httpGet: path: /health port: 3051 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 10

Production Checklist

Security

  • Enable HTTPS/TLS 1.3
  • Configure proper JWT secrets
  • Set up rate limiting
  • Enable audit logging
  • Configure CORS policies
  • Set secure session timeouts

Performance

  • Optimize database connections
  • Enable connection pooling
  • Configure caching policies
  • Set up monitoring
  • Configure load balancing
  • Enable health checks

Multi-Factor Authentication (MFA)

TOTP-Based MFA

ZenoAuth provides RFC 6238 compliant TOTP (Time-based One-Time Password) authentication with backup codes for account recovery.

Initialize MFA Setup

POST /auth/mfa/setup/init
# Response { "secret": "JBSWY3DPEHPK3PXP", "qr_code_uri": "otpauth://totp/ZenoAuth:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=ZenoAuth", "issuer": "ZenoAuth" } # Render QR code for authenticator app

Complete MFA Setup

POST /auth/mfa/setup/verify
{ "code": "123456" } # Response with backup codes { "success": true, "backup_codes": [ "A1B2C3D4", "E5F6G7H8", "I9J0K1L2", "M3N4O5P6", "Q7R8S9T0", "U1V2W3X4", "Y5Z6A7B8", "C9D0E1F2", "G3H4I5J6", "K7L8M9N0" ] }

MFA Login Flow

Two-Step Login
# Step 1: Initial login returns MFA challenge POST /auth/login { "email": "[email protected]", "password": "password" } # Response when MFA is enabled { "mfa_required": true, "mfa_token": "eyJhbGciOiJFZDI1NTE5...", "message": "MFA verification required" } # Step 2: Verify MFA code POST /auth/mfa/verify { "mfa_token": "eyJhbGciOiJFZDI1NTE5...", "code": "123456" } # Response with tokens { "access_token": "eyJhbGciOiJFZDI1NTE5...", "refresh_token": "...", "token_type": "Bearer", "expires_in": 3600 }

MFA Management APIs

GET /auth/mfa/status

Check MFA status
and backup codes remaining

POST /auth/mfa/disable

Disable MFA
(requires current TOTP code)

POST /auth/mfa/backup-codes/regenerate

Generate new backup codes
(invalidates previous)

External SSO Providers

Configure External Identity Providers

Connect OIDC and OAuth 2.0 providers like Google, Microsoft, GitHub, and Okta for seamless Single Sign-On.

POST /api/v1/sso/providers
{ "name": "google", "display_name": "Google", "provider_type": "oidc", "client_id": "your-google-client-id.apps.googleusercontent.com", "client_secret": "your-google-client-secret", "issuer_url": "https://accounts.google.com", "scopes": ["openid", "profile", "email"], "button_text": "Continue with Google", "icon_url": "https://example.com/icons/google.svg", "button_color": "#4285F4", "auto_provision_users": true, "allow_account_linking": true, "enabled": true }

Initiate SSO Login

SSO Flow
# List available providers GET /oauth/sso/providers # Initiate SSO (redirects to provider) GET /oauth/sso/google?redirect_uri=https://app.com/callback # Callback returns tokens GET /oauth/sso/google/callback?code=...&state=...

Supported Providers

  • Google (OIDC)
  • Microsoft / Azure AD (OIDC)
  • GitHub (OAuth 2.0)
  • Okta (OIDC)
  • Any OIDC/OAuth 2.0 provider

Signing Key Management

Ed25519 JWT Signing Keys

ZenoAuth uses Ed25519 elliptic curve cryptography for JWT signing with full key lifecycle management.

Key Operations

Key Management APIs
# List all signing keys GET /api/v1/keys # Get current signing key GET /api/v1/keys/current # Create new key POST /api/v1/keys { "kid": "key-2024-06-01", "activate_immediately": false } # Rotate to new key POST /api/v1/keys/{key_id}/rotate # Revoke key (emergency) POST /api/v1/keys/{key_id}/revoke

JWKS Endpoint

GET /oauth/jwks
{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "kid": "key-2024-01-15", "use": "sig", "alg": "EdDSA", "x": "11qYAYKxCrfVS_7TyWQHOg..." } ] } # Resource servers fetch this to # verify JWT signatures

Key Rotation Best Practices

6-12 mo

Regular rotation
schedule

Grace Period

Old key in JWKS
during transition

Audit Trail

All key operations
logged

Groups & Permissions

Group-Based Access Control

Organize users into groups with associated permissions for role-based access control (RBAC).

Create Group

POST /api/v1/groups
{ "name": "Engineering", "description": "Engineering team members", "permissions": [ "code:read", "code:write", "deploy:staging" ] }

Manage Membership

Group Membership APIs
# Add user to group POST /api/v1/groups/{group_id}/members { "user_id": "550e8400-e29b..." } # Remove user from group DELETE /api/v1/groups/{group_id}/members/{user_id} # Get group members GET /api/v1/groups/{group_id}/members

Default Groups

Administrators

Full system access with admin:* permissions. First user is automatically added.

Users

Basic user access with user:profile permission. All users added by default.

SCIM v2 Provisioning

SCIM User Operations

Create User

POST /scim/v2/Users
{ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], "userName": "[email protected]", "name": { "givenName": "John", "familyName": "Doe" }, "emails": [ { "value": "[email protected]", "primary": true } ], "active": true }

Update User

PATCH /scim/v2/Users/:id
{ "schemas": [ "urn:ietf:params:scim:api:messages:2.0:PatchOp" ], "Operations": [ { "op": "replace", "path": "active", "value": false } ] } # Other operations GET /scim/v2/Users # List users GET /scim/v2/Users/:id # Get user DELETE /scim/v2/Users/:id # Delete user

SCIM Group Operations

Group Management
# Create Group POST /scim/v2/Groups { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Engineering", "members": [ { "value": "user-uuid-1" }, { "value": "user-uuid-2" } ] } # List Groups GET /scim/v2/Groups # Update Group Membership PATCH /scim/v2/Groups/:id { "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [ { "op": "add", "path": "members", "value": [{"value": "new-user-uuid"}] } ] }

SCIM Nested Groups (RFC 7643)

ZenoAuth supports nested groups where groups can contain other groups as members, enabling complex organizational hierarchies with transitive membership.

Nested Group Structure
# Create Parent Group POST /scim/v2/Groups { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Engineering" } # Create Child Group POST /scim/v2/Groups { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"], "displayName": "Backend Team" } # Add Child Group as Member (Nested) PATCH /scim/v2/Groups/engineering-uuid { "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], "Operations": [{ "op": "add", "path": "members", "value": [{ "value": "backend-team-uuid", "type": "Group" }] }] } # GET Group Returns Mixed Members GET /scim/v2/Groups/engineering-uuid { "id": "engineering-uuid", "displayName": "Engineering", "members": [ { "value": "user-1", "type": "User", "display": "john.doe" }, { "value": "backend-team-uuid", "type": "Group", "display": "Backend Team" } ] }

Key Features

  • Transitive Membership: Users in nested groups inherit parent group memberships automatically
  • Circular Reference Prevention: Database-level constraints prevent Group A → Group B → Group A cycles
  • Depth Limit: Supports up to 10 levels of nesting for recursive membership resolution
  • Bidirectional Sync: Works for both inbound (receiving) and outbound (pushing) SCIM provisioning
  • RFC 7643 Compliant: Full compliance with SCIM 2.0 nested groups specification

Service Provider Configuration

SCIM clients can discover capabilities via the ServiceProviderConfig endpoint.

GET /scim/v2/ServiceProviderConfig
{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"], "patch": { "supported": true }, "bulk": { "supported": false }, "filter": { "supported": true, "maxResults": 200 }, "changePassword": { "supported": false }, "sort": { "supported": false }, "etag": { "supported": false }, "authenticationSchemes": [ { "type": "oauthbearertoken", "name": "OAuth Bearer Token", "description": "Authentication via OAuth 2.0 Bearer Token" } ] } # Resource Types Discovery GET /scim/v2/ResourceTypes

Admin Interface Setup

Next.js Admin Dashboard

ZenoAuth includes a modern Next.js 15 admin dashboard with React 19 and shadcn/ui components.

Development Setup

Local Development
# Navigate to admin directory cd zenoauth-admin # Install dependencies npm install # Set API URL export NEXT_PUBLIC_API_URL=http://localhost:3051 # Start development server npm run dev # Admin UI at http://localhost:3050

Features

Admin Pages
# Available admin pages: /users User management /clients OAuth client management /groups Group management /roles-rbac Roles & permissions /analytics Usage analytics /audit Audit logs /pim PIM dashboard /pim/requests Elevation requests /pim/policies Approval policies /pim/break-glass Break-glass access /credential-types VC credential types /issued-credentials Issued VCs /trusted-issuers Trusted issuers /sso-providers SSO configuration /settings System settings

Scope Management

Configure custom OAuth scopes with granular permissions.

Scope API
# Create custom scope POST /api/v1/scopes { "name": "user:profile:read", "description": "Read user profile information" } # List all scopes GET /api/v1/scopes # Validate scopes for an application POST /api/v1/scopes/validate { "scopes": ["openid", "profile", "email"] }

Key Capabilities

React 19

Latest React with
Server Components

TanStack

Query for data
fetching & caching

shadcn/ui

Professional
UI components

Integration Examples

JavaScript/Node.js

Node.js Example
// Express.js middleware const jwt = require('jsonwebtoken'); const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://auth.yourdomain.com/.well-known/jwks.json' }); function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { const signingKey = key.publicKey || key.rsaPublicKey; callback(null, signingKey); }); } function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.sendStatus(401); } jwt.verify(token, getKey, { audience: 'your-client-id', issuer: 'https://auth.yourdomain.com', algorithms: ['EdDSA'] }, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); } // Protected route app.get('/protected', authenticateToken, (req, res) => { res.json({ message: 'Access granted!', user: req.user }); });

Python/Flask

Python Example
# Flask integration import jwt import requests from functools import wraps from flask import Flask, request, jsonify app = Flask(__name__) def get_jwks(): response = requests.get( 'https://auth.yourdomain.com/.well-known/jwks.json' ) return response.json() def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token: return jsonify({'error': 'Token missing'}), 401 try: token = token.split(' ')[1] # Remove 'Bearer ' jwks = get_jwks() # Verify token with Ed25519 public key decoded = jwt.decode( token, key=jwks['keys'][0], algorithms=['EdDSA'], audience='your-client-id', issuer='https://auth.yourdomain.com' ) current_user = decoded except jwt.InvalidTokenError: return jsonify({'error': 'Token invalid'}), 401 return f(current_user, *args, **kwargs) return decorated @app.route('/protected') @token_required def protected(current_user): return jsonify({ 'message': 'Access granted!', 'user': current_user })