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

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>.

FieldTypePurpose
configArc<Config>Immutable configuration parsed at startup
dbsqlx::AnyPoolDatabase connection pool for all persistence
crdtArc<tokio::sync::RwLock<IdpCrdt>>CRDT cluster state; protected by a tokio RwLock
metadataArc<str>Pre-serialised RFC 8414 JSON (built once at startup)
oidc_metadataArc<str>Pre-serialised OIDC Discovery JSON
key_pair_rwArc<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_idArc<str>Stable node identifier (from HOSTNAME env or a fresh UUIDv4)
gss_credOption<Arc<GssServerCred>>GSSAPI server credential; None when keytab is unavailable
ipaArc<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_peersArc<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_nodesArc<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_logArc<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_idpsArc<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_aliasesVec<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 prefixHandler module
/authorize, /token, /jwks, /revoke, /introspect, /userinfo, /par, /device_authorization, /device, /registerroutes::oauth2
/.well-known/oauth-authorization-server, /.well-known/openid-configurationroutes::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 codeAuthCodePayload sealed with AppState::wrapping_key() (the first element of key_pair_rw) via auth::aead.
  • Refresh tokenRefreshTokenPayload sealed with AppState::refresh_key() (the HKDF-derived second element of key_pair_rw).
  • Session cookieSessionClaims sealed with AppState::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

ComponentLibrary
Async runtimetokio
HTTP frameworkaxum 0.8
Databasesqlx 0.8 (SQLite / PostgreSQL / MariaDB via Any backend)
Schema migrationssqlx built-in migrate!
Crypto / AEAD / HMAC / HKDF / RNGnative-ossl
Certificate / key managementsynta-certificate
CMS gossip encryption (ML-KEM-768 + ECDSA P-256)ahdapa-cms
GSSAPI / SPNEGOahdapa-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 enginehbac-crdt — op-based CRDT rule set with OAuth2 axes
WebUIPreact 10 + TypeScript + PatternFly 6, built with Vite 6 (React 19 API via preact/compat)
Serializationserde + serde_json
ConfigurationTOML