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

Demo: two-IdP federation

Location: contrib/demo/federation/

Starts two ahdapa instances on loopback ports 8080 and 8081 and demonstrates section-6 authentication delegation: a downstream IdP (IdP A) redirects users to an upstream IdP (IdP B) for authentication, then maps the returned identity to a local account.

Topology

InstanceRoleURLRealm
IdP A node1 — “Downstream Corp”downstream (relying) IdPhttp://127.0.0.1:8080CORP.LOCAL
IdP A node2 — “Downstream Corp”gossip replica of node1http://127.0.0.1:8082CORP.LOCAL
IdP B — “Partner IdP”upstream (authenticating) IdPhttp://127.0.0.1:8081PARTNER.LOCAL

What it shows

  • Federation loginalice@CORP.LOCAL is linked to bob@PARTNER.LOCAL. Entering alice at IdP A’s login page redirects the browser to IdP B; the user authenticates as bob there, and IdP A issues a local session for alice.
  • Local logincarol@CORP.LOCAL logs in with a local password at IdP A without any federation redirect.
  • Two-stage login UX — the login page first asks for a username, calls GET /api/auth/federated-hint to check for a federation hint, then either redirects to the upstream (federated case) or shows a password field (local case).
  • OIDC dynamic client registration — IdP A registers itself as a client at IdP B via POST /register (RFC 7591) during setup; the assigned client_id is substituted into IdP A’s runtime config.
  • Federated account linking — the admin API (POST /api/admin/federated-accounts) creates the bob@PARTNER.LOCAL → alice@CORP.LOCAL mapping; no manual database editing is needed.
  • Upstream token store — upstream tokens from IdP B are automatically encrypted and stored during federated login; retrievable via SPNEGO (curl --negotiate) when Kerberos tools are installed.
  • Device authorization grant with return_to — a device-flow client requests a device code, then the user authenticates via the federated login flow with return_to=/device?user_code=... so that after upstream authentication the browser lands on the device consent page instead of the default WebUI.
  • Upstream token refresh verification — after a device-flow federation login deposits new upstream tokens, the demo verifies that the previously stored token has been replaced by the fresh one.
  • Workload federation — a CI-runner client exchanges an upstream ID token at the /federation/token endpoint (Basic Auth adapter), and separately exchanges IdP B’s raw access token directly via RFC 8693 token exchange, for a scoped local access token — using a federation policy to resolve the upstream identity to a local service account in both cases.
  • Gossip replication — IdP A runs as a two-node cluster. Federation policies created on node1 are verified to replicate to node2 via CRDT gossip, demonstrating that federation policies are cluster-wide.

Demo accounts

Passwords are generated randomly at startup and printed to the console.

UsernameIdPWhat happens on login at IdP A
aliceA (local)Redirect to IdP B; log in as bob; redirected back as alice
carolA (local)Local password login; no federation redirect
bobB (local)Direct login at IdP B only
dianaB (local)Direct login at IdP B only

Prerequisites

  • ahdapa binary — the script looks in $PATH first (resolved to its full path), then target/release/ahdapa, then target/debug/ahdapa, and falls back to cargo build if none is found.
  • python3 — for JSON parsing in the setup script.
  • curl.
  • openssl — for generating the EC P-256 key used in the upstream client auth stanza.
  • npm — if the WebUI dist/ directory does not yet exist, the script builds it.
  • Ports 8080 and 8081 free.

Running

# Non-interactive (automated test, exits with pass/fail):
contrib/demo/federation/run.sh

# Interactive (run tests, then keep servers up for manual exploration):
contrib/demo/federation/run.sh --interactive

What the script does

  1. Checks that ports 8080 and 8081 are free; exits with an error if not.
  2. Builds the WebUI (webui/dist/) if the directory is absent, then locates or builds the ahdapa binary (PATH → release → debug → cargo build).
  3. Generates a P-256 private key for IdP A’s upstream client auth stanza (stored at /tmp/ahdapa-demo-idpa-upstream.pem).
  4. If Kerberos tools are installed, starts an ephemeral MIT KDC for realm CORP.LOCAL with alice@CORP.LOCAL and HTTP/localhost@CORP.LOCAL principals.
  5. Starts IdP B on port 8081; waits for its discovery document to be served.
  6. Registers IdP A as a public OIDC client at IdP B via POST http://127.0.0.1:8081/register using the demo registration token (demo-federation-setup-token — set in idpb.toml). The response contains the generated client_id.
  7. Writes a runtime config for IdP A at /tmp/ahdapa-demo-idpa-runtime.toml by substituting the real client_id and keytab path into the template.
  8. Starts IdP A on port 8080; waits for its discovery document.
  9. Logs in as alice at IdP A via the admin API, then calls POST /api/admin/federated-accounts to link bob@PARTNER.LOCAL to alice@CORP.LOCAL.
  10. Runs automated tests:
    • Drives a scripted federated login (alice → IdP B → bob → callback).
    • Verifies upstream-token.deposited audit event in idpa.log.
    • If KDC is available: kinit + SPNEGO retrieval of the stored upstream token.
  11. Prints PASS/FAIL summary.
  12. In interactive mode: prints URLs and waits for Ctrl-C.

Example non-interactive output

==> Starting IdP A (Downstream Corp) on :8080…
    Waiting for IdP A. ready.
==> Creating admin session at IdP A (alice)…
==> Linking bob@PARTNER.LOCAL → alice@CORP.LOCAL…
    bob@PARTNER.LOCAL  →  alice@CORP.LOCAL

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Step 1 — drive federated login (alice → bob) to store upstream token
  ✓ Federation redirect to IdP B
  ✓ Got callback URL for IdP A
  ✓ Federation callback completed (HTTP 302)

  Step 2 — verify upstream token was deposited
  ✓ upstream-token.deposited audit event found in idpa.log

  Step 3 — retrieve upstream token via SPNEGO
  ✓ Retrieved upstream token via SPNEGO (HTTP 200)
  ✓ upstream-token.retrieved audit event found in idpa.log

  Step 5 — device authorization grant with federated login + return_to
  ✓ Got device code (user_code: ABCD-EFGH)
  ✓ Federation redirect to IdP B (with return_to)
  ✓ Callback redirected to device verification page (return_to works)
  ✓ Device authorized
  ✓ Device token obtained

  Step 6 — re-retrieve upstream token after device-flow federation
  ✓ Upstream token refreshed after device-flow federation

  Step 8 — workload federation (keyless CI/CD)
  ✓ CI-runner client created
  ✓ Federation policy created
  ✓ ID token minted at IdP B
  ✓ Token exchanged at /federation/token
  ✓ Access token sub matches ci-runner client_id
  ✓ Got access_token from IdP B (… chars)
  ✓ access_token exchange via trusted_issuers succeeded (HTTP 200)
  ✓ access_token exchange sub = ci-runner client_id (…)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  PASS — all federation demo steps succeeded.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Exploring the demo

With --interactive, the servers stay running after the automated tests. Open http://127.0.0.1:8080/ui/ in a browser.

Federated login path:

  1. Enter alice in the username field and press Enter.
  2. The page detects the federation hint and redirects to IdP B’s login form.
  3. Enter bob and the password printed at startup at IdP B.
  4. IdP B redirects back to IdP A’s callback (/internal/callback/partner-idp).
  5. IdP A maps bob@PARTNER.LOCAL to alice@CORP.LOCAL and issues a local session.
  6. The admin panel shows alice as the logged-in user.

Local login path:

  1. Enter carol and press Enter.
  2. A password field appears (no federation hint for carol).
  3. Enter the password printed at startup. Session is established at IdP A directly.

To initiate the federation flow from a client application, redirect to:

http://127.0.0.1:8080/auth/external/partner-idp

Append standard OAuth2 parameters (client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method) as query parameters.

Upstream token store

The demo enables the upstream token store on IdP A (store_on_federation = true). When alice performs a federated login (redirected to IdP B, logs in as bob), the upstream tokens from IdP B are automatically encrypted and stored in the CRDT.

If Kerberos tools are installed (krb5-server, krb5-workstation), the script starts an ephemeral MIT KDC for realm CORP.LOCAL. After a federated login, retrieve the stored upstream token with SPNEGO:

# Get a Kerberos ticket for alice (password printed at startup)
KRB5_CONFIG=/tmp/ahdapa-demo-federation-kdc/krb5.conf \
  kinit alice@CORP.LOCAL

# Retrieve the stored upstream token via SPNEGO.
# Use "localhost" (not 127.0.0.1) so SPNEGO targets HTTP/localhost@CORP.LOCAL.
curl --negotiate -u: -X POST \
  http://localhost:8080/api/upstream-token/retrieve

The response contains the upstream IdP B access token:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 842,
  "scope": "openid profile email"
}

Without Kerberos tools, the token store is still enabled and tokens are stored on federated login, but SPNEGO retrieval is not available. The demo prints a note at startup if Kerberos tools are missing.

Workload federation

Step 8 of the demo exercises keyless CI/CD authentication via the /federation/token endpoint:

  1. A ci-runner client is created at IdP A with token-exchange as its only grant type and allowed_token_exchange_audiences set to IdP B’s issuer URL.
  2. A federation policy maps *@PARTNER.LOCAL (any IdP B user) to the ci-runner client_id with a scope ceiling of openid profile.
  3. An ID token is minted at IdP B via a standard code flow (simulating a CI provider issuing a workload identity token).
  4. The ID token is exchanged at IdP A using Basic Auth:
    curl -u "<ci-runner-client-id>:<id_token>" \
      -X POST http://127.0.0.1:8080/federation/token
    
  5. The resulting access token’s sub claim is verified to match the ci-runner client_id, proving the federation policy resolved the upstream identity to the local service account.
  6. The demo also extracts IdP B’s raw access_token from the same token response and exchanges it directly at IdP A’s /token endpoint via RFC 8693 token exchange with subject_token_type=urn:ietf:params:oauth:token-type:access_token (bypassing the /federation/token Basic Auth adapter used in step 4). IdP A verifies the token against IdP B’s JWKS because IdP B’s issuer is listed in federation.trusted_issuers, then maps its sub to the ci-runner client via the same federation policy used for the ID token exchange above. See Trusting upstream tokens for the full trust model.

In a real deployment, the CI provider (GitHub Actions, GitLab CI) issues the OIDC JWT, and docker login sends it as the password in Basic Auth. No long-lived secrets are stored in the CI configuration.

Configuration notes

FileDescription
idpa.tomlIdP A base config; __IDPA_CLIENT_ID__ placeholder substituted at runtime
idpb.tomlIdP B config; registration_token = "demo-federation-setup-token" enables POST /register
users-idpa.toml.inUsers template for IdP A (alice, carol); passwords substituted at runtime
users-idpb.toml.inUsers template for IdP B (bob, diana); passwords substituted at runtime
clients-idpa.tomlStatic OAuth2 clients for IdP A: token-retriever (upstream token retrieval) and demo-device (device authorization grant)

The upstream IdP stanza in IdP A’s config:

[[federation.upstream_idps]]
id               = "partner-idp"
issuer           = "http://127.0.0.1:8081"
client_id        = "<substituted at runtime>"
private_key_path = "/tmp/ahdapa-demo-idpa-upstream.pem"
scopes           = ["openid", "profile", "email"]
callback_path    = "/internal/callback/partner-idp"

IdP A trusts tokens issued by IdP B:

[federation]
trusted_issuers = ["http://127.0.0.1:8081"]

See also