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"]
        saml2["SAML2 endpoints\n/saml2/metadata · /saml2/sso\n/saml2/slo · /saml2/acs\n/saml2/sp/metadata · /saml2/auth\n/saml2/artifact · /saml2/slo/start"]
        auth["Auth module\nGSSAPI/SPNEGO · Password\nSession cookie · AEAD codes"]
        crdt["CRDT state\nIdpCrdt (RwLock)\nsigning keys · clients · nodes\nSAML2 SP/IdP entries"]
        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\nSAML2 SP/IdP management"]
        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

    subgraph saml_ext["SAML2 Peers"]
        saml_sp["SAML2 Service Providers"]
        saml_idp["Upstream SAML2 IdPs"]
    end

    browser -->|"HTTPS"| oauth2
    browser -->|"HTTPS"| saml2
    device --> oauth2
    service --> oauth2
    oauth2 --> auth
    saml2 --> auth
    auth -->|"GSSAPI token"| kdc
    auth -->|"user lookup"| ldap
    oauth2 --> crdt
    saml2 --> crdt
    crdt --> db
    admin --> crdt
    gossip -->|"POST /api/gossip/sync"| node2
    gossip -->|"POST /api/gossip/sync"| node3
    crdt --> gossip
    webui --> oauth2
    webui --> admin
    saml_sp -->|"AuthnRequest"| saml2
    saml2 -->|"AuthnRequest"| saml_idp
    saml_idp -->|"SAML Response"| saml2

Source layout

src/
  main.rs          Entry point: load config, open DB, build AppState, start gossip, serve
  config.rs        TOML configuration structs (Config, ServerConfig, DbConfig, …)
  audit.rs         Audit event taxonomy (19 event types), recording, overflow/alarm policies,
                   journal query (journalctl subprocess or in-memory/file fallback)
  journal.rs       JournalWriter: systemd journal namespace via Unix datagram, JSONL file
                   fallback, in-memory daemon for tests
  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
    passkey.rs     WebAuthn passkey assertion/registration verification; COSE↔SPKI DER conversion
    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
    saml2/         SAML2 endpoints: IdP metadata, SSO, SLO, artifact resolution,
                   SP ACS, SP metadata, SP auth, SP-initiated SLO
    admin.rs       /api/admin/* endpoints (clients CRUD, key rotation, nodes, refresh families,
                   SAML2 SP/IdP management)
    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)
  ahdapa-saml2/   SAML2 protocol library: data model, XML serialization (quick-xml),
                  binding codecs (HTTP-Redirect, HTTP-POST, Artifact), xmlsec1 FFI
                  (sign_xml, verify_xml_signature — called from SSO, ACS, and SLO
                  route handlers)
  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
key_pair_rwArc<Mutex<([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 credential (gss_initiator) for S4U2Self and machine authentication (e.g. passkeyconfig_show), initiator credential re-acquisition mode (gss_initiator_mode), per-user S4U2Self credential cache, user-attribute cache, passkey UV policy cache, and optional IPA JSON-RPC API client
jti_cacheArc<DashMap<String, i64>>Lock-free JTI replay cache for private_key_jwt and client_secret_jwt assertions. check_and_insert_jti is synchronous.
static_usersOption<Arc<StaticUsers>>Parsed static users file; None when [users] is not configured.
rbacArc<RbacState>Parsed RBAC roles and group-role bindings from [rbac].
auth_limiterArc<…>Per-IP authentication rate limiter (server.auth_rate_limit).
node_kem_privArc<Vec<u8>>Node’s ML-KEM-768 private key (PKCS#8 DER). Used to decrypt inbound gossip messages.
node_gossip_signing_privArc<Vec<u8>>Node’s ECDSA P-256 gossip signing private key (PKCS#8 DER). Used to sign outbound gossip messages.
node_gossip_signing_certArc<Vec<u8>>Node’s self-signed X.509 certificate (DER) for the gossip signing key. Embedded in outbound SignedData.
local_wrapping_key_idArc<RwLock<String>>UUID of the cluster wrapping key held locally. Updated when the key is rotated or pulled from a peer.
base_pathArc<str>URL path prefix (e.g. "/idp") extracted from the issuer URL. Prepended to all route paths.
gossip_clientArc<reqwest::Client>Pre-configured HTTP client for outbound gossip requests (10-second timeout, optional custom CA).
http_clientreqwest::ClientGeneral-purpose HTTP client for upstream claims, metadata fetches, and JWKS/OIDC-discovery fetches via auth::jwks::verify_jwt.
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.
jwks_cacheArc<tokio::sync::RwLock<HashMap<String, auth::jwks::JwksCacheEntry>>>Short-lived cache of remote JWKS responses keyed by jwks_uri, shared by every call site that verifies a JWT against a remote JWKS through auth::jwks::verify_jwt: private_key_jwt/JWT-bearer client authentication, upstream-IdP OIDC-RP login, and RFC 8693 token exchange of an external id_token or access_token subject token. Entries are refreshed after JWKS_CACHE_TTL.
discovery_cacheArc<tokio::sync::RwLock<HashMap<String, auth::jwks::DiscoveryCacheEntry>>>Short-lived cache of OIDC discovery results (issuer → jwks_uri), used by auth::jwks::JwksSource::Discovery so repeated token exchange and federation login flows against the same issuer skip the discovery fetch. Entries are refreshed after DISCOVERY_CACHE_TTL.
gossip_notifyArc<tokio::sync::Notify>Signals the gossip background task to push state immediately after a local CRDT write, rather than waiting for the next periodic interval.
crdt_changedArc<tokio::sync::Notify>Fires after every gossip merge (inbound sync or outbound response processing). Used by the /api/gossip/await-client long-poll endpoint to detect convergence without polling.
cached_signing_keyArc<tokio::sync::RwLock<HashMap<String, (String, BackendPrivateKey)>>>In-memory cache of this node’s JWT signing keys, keyed by algorithm. Populated on first use and invalidated on key rotation. Avoids a DB query on every token issuance.
gossip_statsArc<Mutex<GossipStats>>Runtime gossip statistics (rounds completed, persist errors, wrapping key pull errors, per-peer last-sync timestamps). Served by GET /api/gossip/stats.
spiffe_caOption<Arc<SpiffeCA>>SPIFFE CA state; None when [spiffe] trust_domain is not set. Used by the Workload API gRPC server to issue X.509-SVIDs and JWT-SVIDs.
journalArc<JournalWriter>Audit event writer: sends structured events to the systemd journal namespace (ahdapa) via Unix datagram, or to a JSONL file fallback, or to tracing when neither is available.
audit_stateArc<AuditState>In-memory audit state: event counter, rolling-window violation tracker, halt flag, and consecutive-insert-failure counter for FAU_STG.4 / FAU_ARP.1 policy enforcement.
audit_policyArc<AuditPolicy>Audit policy configuration (overflow threshold, alarm threshold, alarm action). Parsed from [audit] at startup.
has_revoked_sessionsArc<AtomicBool>Fast-path flag: false when the revoked_sessions table is empty. When false, check_session_revoked() skips the database query entirely, eliminating per-request I/O on clusters with no active revocations. Set to true on first revocation and after gossip merge if the peer has revocations.
client_cacheArc<DashMap<String, (u64, ClientEntry)>>In-memory client lookup cache keyed by client_id, with CRDT generation for staleness detection. Avoids taking the CRDT read lock on every token request.
scope_cacheArc<std::sync::RwLock<ScopeClaimsCache>>Cached scope-to-claims mapping rebuilt from the CRDT scope definitions. Invalidated on CRDT generation change.
revoked_tokens_cacheArc<DashMap<String, i64>>In-memory JTI revocation cache for token revocation checks. Entries are populated from the CRDT on startup and updated on gossip merge.
trust_chain_cacheArc<tokio::sync::RwLock<HashMap<String, auth::trust_chain::TrustChainCacheEntry>>>Cache of resolved OIDC Federation trust chains keyed by leaf issuer. TTL is derived from the shortest exp in the chain.
mds_indexArc<tokio::sync::RwLock<Option<auth::fido_mds::MdsIndex>>>Cached FIDO Metadata Service index for attestation verification.

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
/saml2/*routes::saml2 — IdP metadata, SSO, SLO, artifact resolution; SP ACS, metadata, auth, SLO start
/api/admin/*routes::admin — clients CRUD, key rotation, nodes, refresh families, HBAC policies (/api/admin/hbac), SAML2 SP/IdP management
/api/gossip/*routes::gossip
/ui/*tower-http::ServeDir (Preact SPA)

3. Authorization code flow

sequenceDiagram
    participant B as Browser
    participant A as Ahdapa
    participant IDP as Auth (SPNEGO/Password)

    B->>A: GET /authorize (PKCE code_challenge)
    A->>IDP: SPNEGO attempt (401 Negotiate) or session cookie check
    alt Unauthenticated
        A-->>B: redirect to /ui/auth/login
    else Authenticated + skip_consent
        A->>A: build AuthCodePayload → AEAD-encrypt
        A-->>B: redirect to redirect_uri
    else Authenticated + consent required
        A->>A: build ConsentPayload → seal as AEAD cookie
        A-->>B: redirect to /ui/auth/consent
        B->>A: GET /api/auth/consent
        A-->>B: display client name + scopes
        B->>A: POST /api/auth/consent {allow: true}
        A->>A: decrypt consent cookie → build AuthCodePayload → AEAD-encrypt
        A-->>B: return redirect_to URL
    end

    B->>A: GET {redirect_uri}?code=…&iss=…

    participant C as Client
    C->>A: POST /token (code + code_verifier)
    A->>A: decrypt auth code → verify PKCE
    A-->>C: 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 either on a gossip_notify signal (fired immediately after any local CRDT write) or after gossip.interval_secs seconds (passive fallback). It prepares all per-peer payloads under a single CRDT read lock, then sends HTTP POSTs to all peers concurrently via tokio::task::JoinSet, and processes responses sequentially. Wall-clock gossip time is O(max single-peer round-trip) regardless of cluster size. 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
SAML2 protocolahdapa-saml2 (quick-xml + flate2 + libxmlsec1-openssl FFI)
WebUIPreact 10 + TypeScript + PatternFly 6, built with Vite 6 (React 19 API via preact/compat)
Serializationserde + serde_json
ConfigurationTOML