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 — “Downstream Corp”downstream (relying) IdPhttp://127.0.0.1:8080CORP.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.

Demo accounts

UsernamePasswordIdPWhat happens on login at IdP A
alicealice123A (local)Redirect to IdP B; log in as bob / bob123; redirected back as alice
carolcarol123A (local)Local password login; no federation redirect
bobbob123B (local)Direct login at IdP B only
dianadiana123B (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

contrib/demo/federation/run.sh

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. Starts IdP B on port 8081; waits for its discovery document to be served.
  5. 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.
  6. Writes a runtime config for IdP A at /tmp/ahdapa-demo-idpa-runtime.toml by substituting the real client_id into the __IDPA_CLIENT_ID__ placeholder in idpa.toml.
  7. Starts IdP A on port 8080; waits for its discovery document.
  8. 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.
  9. Prints the URLs and waits until Ctrl-C. Press Ctrl-C to stop both servers; the script traps SIGINT and kills both processes.

Example startup output

Using binary: /usr/bin/ahdapa
==> Starting IdP B (Partner IdP) on :8081…
    Waiting for IdP B..................... ready.
==> Registering IdP A as OIDC client at IdP B…
    client_id=a1b2c3d4-e5f6-...
==> Generating IdP A runtime config…
==> 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

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  TWO-IdP FEDERATION DEMO

  IdP A — Downstream Corp    http://127.0.0.1:8080
    WebUI:       http://127.0.0.1:8080/ui/
    Local users: alice / alice123  |  carol / carol123

  IdP B — Partner IdP        http://127.0.0.1:8081
    WebUI:       http://127.0.0.1:8081/ui/
    Local users: bob / bob123  |  diana / diana123

  FEDERATION (§6 auth delegation)
    Start external auth at IdP A:
      http://127.0.0.1:8080/auth/external/partner-idp

  Press Ctrl-C to stop both servers.

Exploring the demo

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 / bob123 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 carol123. 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.

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.tomlLocal users for IdP A (alice, carol)
users-idpb.tomlLocal users for IdP B (bob, diana)

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