Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Kanidm Dashboard is a modern web console for Kanidm — the identity provider. It is a SolidJS single-page application that provides an admin interface and user self-service portal over the Kanidm REST API.

Who is this for?

  • Organisations running Kanidm who want an admin UI and user portal without building their own frontend.
  • Operators who prefer static deploy-time config (theme, branding, login message) over a writeable app database.
  • Developers integrating Kanidm OAuth2 who need a reference client implementation.

Design principles

  • API-spine first. The dashboard talks to Kanidm through a same-origin proxy — it is a static asset bundle served alongside Kanidm’s /v1, /oauth2, and /ui routes by a reverse proxy such as Caddy.
  • Adapter seam. All data access goes through a DashboardDataSource interface with two adapters: KanidmDataSource (real API) and MockDataSource (in-memory demo). Switching between them is a single config field.
  • Static config. Deploy-time settings (theme, company name, logo, login message) live in a JSON file — no database required. Kanidm-native branding (domain display name, domain image, OAuth2 app icons) is read from and written to Kanidm directly.

Features

  • Admin console — manage people, groups, and OAuth2 applications with review-before-submit wizards and access-impact previews.
  • Self-service portal — users can manage credentials: password, TOTP, passkeys, SSH public keys, RADIUS, and Unix account settings.
  • Credential update wizard — multi-step state machine for enrolling new credentials via reset tokens.
  • Relationship explorer — visualise how people, groups, and applications connect.
  • Native Kanidm branding — domain display name, domain image, and OAuth2 application icons.
  • OAuth2 / OIDC consent flow — built-in consent page and native OAuth2 authorization endpoint.
  • E2E test suite — Playwright tests against a real Kanidm server covering admin workflows, self-service, OAuth2 consent, credential resets, WebAuthn passkeys, and recovery email.

Getting Started

Prerequisites

  • Node.js ≥ 22
  • Vite+ (vp) — the project uses vp (Vite+) as its toolchain entry point
  • just — command runner for project recipes
  • openapi-nexus — Rust CLI for generating the TypeScript SDK
  • Kanidm (optional) — for real-API development and E2E tests

Quick start

# Install dependencies
vp install

# Generate the Kanidm TypeScript SDK (required before vp dev or vp build)
just generate-sdk

# Start the dev server
vp dev

Open http://localhost:5173. The default config uses Kanidm mode — use scripts/fixtures/dashboard.config.mock.json as public/dashboard.config.json for pre-seeded demo data without a Kanidm backend.

Local Kanidm

Start Kanidm and generate TLS certificates:

./scripts/dev-kanidm-bootstrap.sh

This starts Kanidm on https://localhost:18443. During vp dev, Vite proxies Kanidm routes (/v1, /oauth2, /.well-known, /docs) to that server.

Recover the idm_admin password:

docker compose -f deploy/local/docker-compose.yml exec kanidm \
  kanidmd recover-account idm_admin -c /data/server.toml

Copy .env.local.example to .env.local with the recovered password.

Same-origin preview

Build the dashboard and serve it through Caddy for a production-like setup:

vp build
docker compose -f deploy/local/docker-compose.yml up -d dashboard-proxy

Open https://localhost:9443. Caddy serves dashboard assets and proxies Kanidm routes.

Commands

CommandDescription
just installInstall dependencies
just generate-sdkRegenerate the Kanidm TypeScript SDK
just checkType-check and lint
just testRun unit tests
just buildProduction build to dist/
just auditVerify production build integrity
just ci-fastRun all fast quality checks
just bookBuild the documentation site
just book-serveServe documentation with live reload

Architecture

Request flow

browser
  │
  ├─ /                   → dashboard SPA (SolidJS + Vite)
  ├─ /v1/*               → Kanidm REST API (proxied by Caddy / Vite dev server)
  ├─ /oauth2/*           → Kanidm OAuth2 endpoints
  └─ /dashboard.config.json → static deploy-time config

Source layout

src/
  domain.ts             Shared types: Person, Group, Application, ConsoleState
  data-source.ts        DashboardDataSource interface + KanidmDataSource + MockDataSource
  store.tsx             SolidJS context: state, auth, credential operations
  kanidm-auth.ts        Auth state machine: password, TOTP, passkey, backup code, security key
  kanidm-mappers.ts     Kanidm API response → domain model mapping
  kanidm-composite.ts   Multi-step create operations: groups, OAuth2 apps
  kanidm-error.ts       HTTP error types and auth-failure detection
  seed.ts               Initial state and demo fixtures
  App.tsx               Router, layout shells, and page components
  components/           Shared UI components: ErrorBox, OptionGrid, GlassPanel, etc.
  generated/            TypeScript SDK generated from Kanidm OpenAPI spec

Data source seam

The DashboardDataSource interface defines 31 CRUD methods. Two adapters implement it:

  • KanidmDataSource — calls the Kanidm REST API via a generated TypeScript SDK. The SDK is generated from spec/kanidm-openapi.json using openapi-nexus.
  • MockDataSource — in-memory state persisted to localStorage. Seeds from src/seed.ts with demo people, groups, and applications.

Switching between them is a single config field (dataSource.mode) — no code changes required.

Store

store.tsx provides a SolidJS context (ConsoleProvider) that wraps the data source. It exposes:

  • State — reactive ConsoleState with people, groups, applications, and branding.
  • Auth — login flows (password, TOTP, passkey, backup code, security key).
  • Mutations — CRUD operations that go through mutateKanidm() (writes) or readKanidm() (reads), both handling error recovery and state reload.

Auth flow

Kanidm uses a stepped authentication protocol. Each step returns an X-KANIDM-AUTH-SESSION-ID header that must be sent with the next request. The dashboard implements this state machine in kanidm-auth.ts:

  1. init2 — begin authentication with username
  2. choose — the server responds with available mechanisms
  3. begin — select a mechanism (password, passkey, etc.)
  4. cred — submit credentials

Credential update state machine

The credential update wizard (src/pages/reset-credentials.tsx) and self-service update page (src/pages/enrol.tsx) use Kanidm’s multi-step credential protocol:

  1. _exchange_intent — exchange an admin-issued reset token for a session, or /v1/person/{id}/_credential/_update — begin an authenticated self-service session
  2. _status — query current credential state
  3. _update — stage credential changes (password, TOTP, passkeys, etc.)
  4. _commit — commit all staged changes atomically

Configuration

The dashboard reads a single JSON file at /dashboard.config.json:

{
  "dataSource": {
    "mode": "kanidm",
    "apiBasePath": "",
    "openApiPath": "/docs/v1/openapi.json"
  },
  "siteName": "My Org",
  "logoUrl": "https://example.com/logo.svg",
  "loginMessage": "Sign in with your organisation account",
  "adminGroup": "idm_admins",
  "theme": {
    "mode": "system",
    "preset": "blue"
  }
}

Fields

FieldTypeDescription
dataSource.mode"kanidm" | "mock""kanidm" for real API, "mock" for demo data
dataSource.apiBasePathstringBase path for the Kanidm API (empty for same-origin)
dataSource.openApiPathstringPath to the Kanidm OpenAPI spec
siteNamestringSite title shown in the browser tab and login page
logoUrlstringFallback logo when native Kanidm domain branding is unavailable
loginMessagestringMessage displayed on the login page
adminGroupstringKanidm group whose members get admin console access
theme.mode"light" | "dark" | "system"Colour scheme mode
theme.presetstringColour preset name

Static vs. native branding

The dashboard has two branding layers:

  1. Static config (dashboard.config.json) — deploy-time settings: company name, fallback logo, login message, and theme. Update the file and restart.
  2. Native Kanidm — domain display name, domain image, and OAuth2 application icons are read from (and written to) Kanidm directly via the REST API. These take precedence over static config when available.

Demo mode

For development and demos without a Kanidm backend, use scripts/fixtures/dashboard.config.mock.json:

cp scripts/fixtures/dashboard.config.mock.json public/dashboard.config.json
vp dev

The mock data source pre-seeds people, groups, and applications so every page is explorable immediately.

Development

Setup

vp install
just generate-sdk
vp dev

See the Getting Started guide for detailed prerequisites and local Kanidm setup.

Project scripts

just check     # Type-check and lint
just test      # Run unit tests
just build     # Production build
just audit     # Verify production build integrity
just ci-fast   # Run all fast quality checks

Code conventions

  • TypeScript strict mode — all code is fully typed; any is avoided.
  • SolidJS reactivity — use signals, createEffect, and setState; avoid direct DOM manipulation.
  • Component structure — shared UI components live in src/components/; page components are defined in src/App.tsx.
  • Data access — always go through the DashboardDataSource interface; never call the SDK directly from components.

Adding a new page

  1. Define the route in App.tsx in the SwitchPublic or SwitchPrivate component.
  2. Create the page component in App.tsx (or a new file in src/pages/).
  3. Use useConsole() to access state and mutations.
  4. Follow the existing patterns for error handling (ErrorBox), busy states, and review-before-submit flows.

SDK Generation

The Kanidm TypeScript SDK is generated from the checked-in OpenAPI spec at spec/kanidm-openapi.json using openapi-nexus.

Prerequisites

Install openapi-nexus:

cargo install --path ../openapi-nexus

Generate

just generate-sdk

This runs openapi-nexus generate with:

  • Input: spec/kanidm-openapi.json
  • Output: src/generated/kanidm-sdk/
  • Generator: typescript-fetch
  • Naming: camelCase property naming, PascalCase file naming
  • Target: ES2022, ESNext modules

The generated SDK includes:

  • API classes in apis/ — one per Kanidm tag (PersonApi, GroupApi, Oauth2Api, etc.)
  • Model types in models/ — TypeScript interfaces with fromJSON / toJSON converters
  • Runtime in runtime/Configuration, fetch wrapper, response types

Version control

The src/generated/ directory is excluded from version control via .gitignore. Run just generate-sdk after cloning the repository and after updating spec/kanidm-openapi.json.

Testing

Unit tests

just test

Tests cover:

  • Data source seamMockDataSource and KanidmDataSource behaviour
  • Auth flows — login with password, password+TOTP, backup code
  • Composite operations — group and OAuth2 application creation
  • Mappers — Kanidm API response parsing and domain model mapping

E2E tests

# Full suite against a real Kanidm
just e2e-kanidm

# WebAuthn / passkey test
just e2e-webauthn

# Recovery email test
just e2e-recovery-mail

The main E2E suite (scripts/e2e-real-kanidm.mjs) uses Playwright with Chromium against a real Kanidm instance. It verifies 22 behaviours:

  1. Expired session redirect
  2. Admin login
  3. Group creation (parent and child)
  4. Person creation with group membership
  5. Group membership toggling
  6. Nested relationship resolution
  7. OAuth2 application creation
  8. Application image upload and reset
  9. Domain image upload and reset
  10. Credential setup (password, TOTP, backup codes) via reset token
  11. Native OAuth2 discovery, consent, and access denial
  12. Non-admin portal login with backup code
  13. Non-admin route guards (admin pages redirect to portal)
  14. Non-admin mutation denial (direct API calls are rejected)
  15. Profile read-only enforcement
  16. RADIUS self-service
  17. SSH public key management
  18. Reauth flow
  19. Session revocation
  20. Unix credential self-service
  21. Logout
  22. Fixture cleanup

Each run creates unique test fixtures and cleans them up on completion, even when tests fail.

Requirements

  • KANIDM_PASSWORD in .env.local
  • Running Kanidm instance (via ./scripts/dev-kanidm-bootstrap.sh)
  • Caddy proxy (via docker compose -f deploy/local/docker-compose.yml up -d dashboard-proxy)

Production artifact audit

just audit

Verifies the production build output: checks that all expected files exist, the JavaScript bundle is non-empty, SPA fallback works, and the config file is valid.

Deployment

The dashboard is distributed as a container image that bundles the static SPA with an embedded Caddy reverse proxy. Caddy serves the dashboard and proxies Kanidm API routes internally — you only need to expose one port.

Quick start

docker run -d -p 8080:8080 \
  -e KANIDM_UPSTREAM=https://your-kanidm-server:8443 \
  ghcr.io/adamcavendish/kanidm-dashboard:0.0.2

Open http://localhost:8080.

If your Kanidm server runs in the same Docker network, use the container name as the upstream:

# docker-compose.yml
services:
  kanidm:
    # ... your existing Kanidm setup ...

  dashboard:
    image: ghcr.io/adamcavendish/kanidm-dashboard:0.0.2
    environment:
      KANIDM_UPSTREAM: https://kanidm:8443
    ports:
      - "8080:8080"

A full reference compose file is available at deploy/container/docker-compose.yml.

TLS between dashboard and Kanidm

The dashboard communicates with Kanidm over HTTPS. The container’s Caddy server uses the system trust store — it works out of the box when Kanidm has a publicly trusted certificate (e.g. Let’s Encrypt).

Self-signed certificates

If Kanidm uses a self-signed certificate (the default when setting up kanidmd), you need two things:

  1. Mount the Kanidm CA certificate into the container
  2. Mount a custom Caddyfile that trusts it

Where to find chain.pem: Kanidm generates this during initial setup. It is typically at /data/certs/chain.pem inside the Kanidm container, or in the directory you mounted to kanidm’s /data/certs.

Custom Caddyfile (dashboard-caddyfile):

{
	auto_https off
}

:8080 {
	encode zstd gzip

	@dashboardConfig path /dashboard.config.json
	handle @dashboardConfig {
		header Cache-Control "no-store"
		root * /config
		file_server
	}

	@kanidm path /ui* /v1* /oauth2* /pkg* /hpkg* /.well-known* /docs* /status
	handle @kanidm {
		reverse_proxy {$KANIDM_UPSTREAM:https://kanidm:8443} {
			transport http {
				tls_server_name kanidm.example.com
				tls_trusted_ca_certs /certs/chain.pem
			}
		}
	}

	handle {
		root * /srv/dashboard
		try_files {path} /index.html
		file_server
	}
}

Replace kanidm.example.com with your Kanidm server’s domain name — the one in the certificate’s Subject Alternative Name.

Docker Compose:

dashboard:
  image: ghcr.io/adamcavendish/kanidm-dashboard:0.0.2
  environment:
    KANIDM_UPSTREAM: https://kanidm:8443
  volumes:
    - ./dashboard-caddyfile:/etc/caddy/Caddyfile:ro
    - ./certs/chain.pem:/certs/chain.pem:ro
  ports:
    - "8080:8080"
DirectivePurpose
tls_server_nameOverrides the TLS SNI to match the certificate’s domain. The Docker container hostname (kanidm) differs from the domain in the certificate.
tls_trusted_ca_certsTrusts the Kanidm CA certificate so the self-signed cert is accepted.
tls_insecure_skip_verifyAvoid. Disables all TLS verification — accepts any certificate. Use only as a temporary workaround.

Public certificates

No extra configuration is needed. The dashboard’s Caddy server trusts public CAs by default. Set KANIDM_UPSTREAM to your Kanidm server’s HTTPS URL and the TLS handshake will verify normally.

Reverse proxy (standalone)

If you prefer to run the dashboard without the container’s embedded Caddy, serve dist/ with your own reverse proxy:

# Build from source
vp build
# Serve dist/ with your web server, proxying Kanidm routes

Example Caddy configuration:

kanidm.example.com {
    handle /v1/* {
        reverse_proxy https://kanidm-server:8443
    }
    handle /oauth2/* {
        reverse_proxy https://kanidm-server:8443
    }
    handle {
        root * /srv/dashboard
        try_files {path} /index.html
        file_server
    }
}

Configuring the dashboard

Place a dashboard.config.json at the web root (next to index.html). The container image includes a default config. See Configuration for all options.

To use a custom config with the container:

volumes:
  - ./my-dashboard.config.json:/config/dashboard.config.json:ro

CI/CD

Container images are published to ghcr.io/adamcavendish/kanidm-dashboard on every semver tag ([0-9]*). See .github/workflows/container-image.yml.