Architecture
This chapter describes the overall structure of Ahdapa, the key modules, and the full request lifecycle from a TCP connection to an HTTP response.
System architecture
graph TB
subgraph clients["OAuth2 Clients / Browsers"]
browser["Browser (Authorization Code)"]
device["CLI / IoT (Device Flow)"]
service["Service (Client Credentials)"]
end
subgraph ahdapa["Ahdapa Node"]
direction TB
oauth2["OAuth2 endpoints\n/authorize · /token · /jwks\n/revoke · /introspect · /userinfo\n/par · /device_authorization"]
auth["Auth module\nGSSAPI/SPNEGO · Password\nSession cookie · AEAD codes"]
crdt["CRDT state\nIdpCrdt (RwLock)\nsigning keys · clients · nodes"]
db[("Local DB\nSQLite / Postgres / MariaDB\nCRDT snapshots + ephemeral tables")]
webui["WebUI\nPreact SPA\n/ui/auth/* · /ui/admin/*"]
admin["Admin API\n/api/admin/*\nclients · keys · nodes · families"]
gossip["Gossip loop\n/api/gossip/sync\nCMS (ML-KEM-768 + ECDSA P-256)"]
end
subgraph infra["FreeIPA Infrastructure"]
kdc["KDC (Kerberos)"]
ldap["LDAP (FreeIPA)"]
end
subgraph peers["Peer Nodes"]
node2["Ahdapa Node 2"]
node3["Ahdapa Node 3"]
end
browser -->|"HTTPS"| oauth2
device --> oauth2
service --> oauth2
oauth2 --> auth
auth -->|"GSSAPI token"| kdc
auth -->|"user lookup"| ldap
oauth2 --> crdt
crdt --> db
admin --> crdt
gossip -->|"POST /api/gossip/sync"| node2
gossip -->|"POST /api/gossip/sync"| node3
crdt --> gossip
webui --> oauth2
webui --> admin
Source layout
src/
main.rs Entry point: load config, open DB, build AppState, start gossip, serve
config.rs TOML configuration structs (Config, ServerConfig, DbConfig, …)
crdt/
mod.rs IdpCrdt and the four CRDT primitives: GrowSet, LwwRegister, OrMap, LwwMap;
load_from_db / persist_to_db / merge
db/
mod.rs Database initialization (open, migrations, WAL mode)
schema.rs Row types mirroring every CRDT and ephemeral table
auth/
mod.rs AuthResult enum; AuthError; public re-exports
aead.rs AES-256-GCM seal / open helpers (native-ossl)
code.rs Authorization code payload: AEAD-encrypted JSON blob
consent.rs Consent cookie payload: AEAD-encrypted pending authorization state
cookie.rs Session cookie: seal / unseal (SessionClaims)
gssapi.rs SPNEGO acceptor via ahdapa-gssapi
ipa.rs FreeIPA attribute lookup: IPA JSON-RPC API (default) or LDAP direct
password.rs Password-based authentication via LDAP simple bind
pkce.rs PKCE S256 code_challenge / code_verifier verification
refresh.rs Refresh token payload: AEAD-encrypted, rotation + replay detection
routes/
mod.rs AppState definition; bootstrap helpers; build() — assembles the axum Router
oauth2.rs All OAuth2 / OIDC endpoints (authorize, token, jwks, revoke, …)
discovery.rs /.well-known/oauth-authorization-server and /.well-known/openid-configuration
admin.rs /api/admin/* endpoints (clients CRUD, key rotation, nodes, refresh families)
gossip.rs /api/gossip/sync handler and background gossip loop
topology.rs FreeIPA topology-based gossip peer discovery (ipa_topology)
webui/
index.html Entry HTML — inline script for flash-free theme application
src/ Preact + TypeScript source
main.tsx Preact entry point — wraps App with ThemeProvider, ToastProvider
App.tsx React Router setup (basename="/ui")
api.ts Typed fetch helpers for admin and auth APIs
pf.tsx Custom PatternFly 6 component library (cx, NavSection, Breadcrumb,
Pagination, Modal with focus trap, FormSelect, Table, etc.)
theme.tsx ThemeProvider / useTheme() — dark mode with localStorage persistence
toast.tsx ToastProvider / useToast() — portal-based PF6 AlertGroup notifications
auth/ User-facing auth pages (Login, Consent, Device, Error)
admin/ Operator admin pages and AdminLayout (nav groups, breadcrumb, branding)
user/ User self-service pages (ProfilePage)
dist/ Built SPA (generated by `npm run build`)
migrations/
sqlite/0001_initial.sql SQLite DDL
postgres/0001_initial.sql Postgres DDL
mariadb/0001_initial.sql MariaDB DDL
crates/
ahdapa-gssapi/ Safe Rust GSSAPI bindings (fork of akamu-gssapi)
ahdapa-jose/ JWT signing / verification primitives (fork of akamu-jose)
ahdapa-ldap/ Safe Rust OpenLDAP bindings (fork of akamu-ldap)
hbac-crdt/ Identity HBAC policy engine: op-based CRDT rule set, OAuth2 evaluation
AppState
Defined in src/routes/mod.rs. Every axum handler receives a clone of AppState via axum’s State extractor. Cloning is cheap: all fields are Arc<T>.
| Field | Type | Purpose |
|---|---|---|
config | Arc<Config> | Immutable configuration parsed at startup |
db | sqlx::AnyPool | Database connection pool for all persistence |
crdt | Arc<tokio::sync::RwLock<IdpCrdt>> | CRDT cluster state; protected by a tokio RwLock |
metadata | Arc<str> | Pre-serialised RFC 8414 JSON (built once at startup) |
oidc_metadata | Arc<str> | Pre-serialised OIDC Discovery JSON |
key_pair_rw | Arc<std::sync::RwLock<([u8; 32], [u8; 32])>> | Pair of (wrapping key, refresh sub-key) held under one lock so key rotation is always atomic. wrapping_key() and refresh_key() are cheap helpers that take a read lock. |
node_id | Arc<str> | Stable node identifier (from HOSTNAME env or a fresh UUIDv4) |
gss_cred | Option<Arc<GssServerCred>> | GSSAPI server credential; None when keytab is unavailable |
ipa | Arc<IpaState> | All IPA/LDAP runtime state: server URI, pre-computed DN paths (from rootDSE discovery), GSSAPI initiator for S4U2Self, per-user S4U2Self credential cache, user-attribute cache, passkey UV policy cache, and optional IPA JSON-RPC API client |
dynamic_peers | Arc<tokio::sync::RwLock<Vec<String>>> | Gossip peer URLs discovered from the IPA replication topology (ipa_topology = true). Empty when ipa_topology = false. Merged with the static gossip.peers list at each gossip round. Updated by topology::run_topology_refresh. |
dynamic_allowed_nodes | Arc<tokio::sync::RwLock<HashSet<String>>> | Hostnames of IPA replicas discovered from the topology, automatically added to the gossip admission allowlist. Merged with gossip.allowed_node_ids at each admission check. Updated by topology::run_topology_refresh. |
hbac_log | Arc<tokio::sync::RwLock<hbac_crdt::OpLog>> | Authoritative op-log for Identity HBAC policies. Mutations (create, patch, delete rule) are appended here and the materialised RuleSet is mirrored into crdt.hbac_rules for gossip. Read by the token endpoint to evaluate evaluate_oauth2 before token issuance. |
ipa_upstream_idps | Arc<tokio::sync::RwLock<Vec<UpstreamIdpConfig>>> | IPA-sourced upstream IdP registrations, refreshed every 300 seconds from cn=idp,<suffix> LDAP objects. Empty when [ipa] gssapi is not configured. Searched by find_upstream() after the static config.federation.upstream_idps list; CRDT ACR/AMR overrides from crdt.ipa_idp_overrides are applied to the cloned entry before it is returned. |
ipa_issuer_aliases | Vec<String> | Per-node issuer aliases auto-derived from [gssapi] initiator_principal (node FQDN) and the ipa-ca.<realm> DNS alias. Supplements any manually configured server.issuer_aliases. Used by accepted_origins() and accepted_issuers() for WebAuthn passkey origin validation, backchannel-logout aud, and client_assertion aud acceptance. |
Request lifecycle
1. TCP accept and HTTP parsing
tokio accepts a TCP connection. axum passes it to the hyper HTTP/1.1 codec. TraceLayer emits a tracing span for each request.
2. Route dispatch
axum matches the request against the router assembled in routes::build:
| Path prefix | Handler module |
|---|---|
/authorize, /token, /jwks, /revoke, /introspect, /userinfo, /par, /device_authorization, /device, /register | routes::oauth2 |
/.well-known/oauth-authorization-server, /.well-known/openid-configuration | routes::discovery |
/api/admin/* | routes::admin — clients CRUD, key rotation, nodes, refresh families, HBAC policies (/api/admin/hbac) |
/api/gossip/* | routes::gossip |
/ui/* | tower-http::ServeDir (Preact SPA) |
3. Authorization code flow
Browser → GET /authorize (with PKCE code_challenge)
└─ SPNEGO attempt (401 Negotiate) or session cookie check
└─ if unauthenticated: redirect to /ui/auth/login
└─ if authenticated:
build ConsentPayload → seal as AEAD cookie → redirect to /ui/auth/consent
Browser → GET /ui/auth/consent (Preact SPA)
└─ fetch GET /api/auth/consent → display client name + scopes
└─ user clicks Allow → POST /api/auth/consent {allow: true}
└─ decrypt consent cookie → build AuthCodePayload → AEAD-encrypt → return redirect_to URL
Browser → GET {redirect_uri}?code=<AEAD-encrypted-code>&iss=...
Client → POST /token (code + code_verifier)
└─ decrypt auth code → verify PKCE → issue JWT access token + refresh token
4. Token issuance
All tokens are issued in routes/oauth2.rs. The actual cryptographic primitives live in src/auth/:
- JWT access token — signed with the active JWT signing key from the CRDT (algorithm set by
[server] jwt_signing_algorithm, default ES256). - Authorization code —
AuthCodePayloadsealed withAppState::wrapping_key()(the first element ofkey_pair_rw) viaauth::aead. - Refresh token —
RefreshTokenPayloadsealed withAppState::refresh_key()(the HKDF-derived second element ofkey_pair_rw). - Session cookie —
SessionClaimssealed withAppState::wrapping_key().
5. CRDT read/write
Handlers acquire a read lock for lookups and a write lock only for mutations:
#![allow(unused)]
fn main() {
// Read (non-blocking for other readers):
let crdt = state.crdt.read().await;
let client = crdt.clients.get(&client_id);
// Write (exclusive):
let mut crdt = state.crdt.write().await;
crdt.active_kid.set(kid, now, &state.node_id);
}
After any CRDT mutation, the handler (or gossip handler) calls crdt.persist_to_db(&state.db) to flush the snapshot.
6. Background gossip
A background tokio task (routes::gossip::run) wakes every gossip.interval_secs seconds and pushes the full CRDT state to each configured peer. See Gossip Protocol for details.
Technology stack
| Component | Library |
|---|---|
| Async runtime | tokio |
| HTTP framework | axum 0.8 |
| Database | sqlx 0.8 (SQLite / PostgreSQL / MariaDB via Any backend) |
| Schema migrations | sqlx built-in migrate! |
| Crypto / AEAD / HMAC / HKDF / RNG | native-ossl |
| Certificate / key management | synta-certificate |
| CMS gossip encryption (ML-KEM-768 + ECDSA P-256) | ahdapa-cms |
| GSSAPI / SPNEGO | ahdapa-gssapi (fork of akamu-gssapi) |
| LDAP (FreeIPA user lookup) | ahdapa-ldap (fork of akamu-ldap) |
| System userdb lookup (optional) | ahdapa-varlink via kirmes (io.systemd.UserDatabase); enabled with --features varlink |
| PAM authentication backend (optional) | ahdapa-pam — inline libpam FFI; enabled with --features pam |
| Identity HBAC policy engine | hbac-crdt — op-based CRDT rule set with OAuth2 axes |
| WebUI | Preact 10 + TypeScript + PatternFly 6, built with Vite 6 (React 19 API via preact/compat) |
| Serialization | serde + serde_json |
| Configuration | TOML |