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
| Instance | Role | URL | Realm |
|---|---|---|---|
| IdP A — “Downstream Corp” | downstream (relying) IdP | http://127.0.0.1:8080 | CORP.LOCAL |
| IdP B — “Partner IdP” | upstream (authenticating) IdP | http://127.0.0.1:8081 | PARTNER.LOCAL |
What it shows
- Federation login —
alice@CORP.LOCALis linked tobob@PARTNER.LOCAL. Enteringaliceat IdP A’s login page redirects the browser to IdP B; the user authenticates asbobthere, and IdP A issues a local session foralice. - Local login —
carol@CORP.LOCALlogs 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-hintto 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 assignedclient_idis substituted into IdP A’s runtime config. - Federated account linking — the admin API (
POST /api/admin/federated-accounts) creates thebob@PARTNER.LOCAL → alice@CORP.LOCALmapping; no manual database editing is needed.
Demo accounts
| Username | Password | IdP | What happens on login at IdP A |
|---|---|---|---|
| alice | alice123 | A (local) | Redirect to IdP B; log in as bob / bob123; redirected back as alice |
| carol | carol123 | A (local) | Local password login; no federation redirect |
| bob | bob123 | B (local) | Direct login at IdP B only |
| diana | diana123 | B (local) | Direct login at IdP B only |
Prerequisites
ahdapabinary — the script looks in$PATHfirst (resolved to its full path), thentarget/release/ahdapa, thentarget/debug/ahdapa, and falls back tocargo buildif 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 WebUIdist/directory does not yet exist, the script builds it.- Ports 8080 and 8081 free.
Running
contrib/demo/federation/run.sh
What the script does
- Checks that ports 8080 and 8081 are free; exits with an error if not.
- Builds the WebUI (
webui/dist/) if the directory is absent, then locates or builds theahdapabinary (PATH → release → debug →cargo build). - Generates a P-256 private key for IdP A’s upstream client auth stanza (stored
at
/tmp/ahdapa-demo-idpa-upstream.pem). - Starts IdP B on port 8081; waits for its discovery document to be served.
- Registers IdP A as a public OIDC client at IdP B via
POST http://127.0.0.1:8081/registerusing the demo registration token (demo-federation-setup-token— set inidpb.toml). The response contains the generatedclient_id. - Writes a runtime config for IdP A at
/tmp/ahdapa-demo-idpa-runtime.tomlby substituting the realclient_idinto the__IDPA_CLIENT_ID__placeholder inidpa.toml. - Starts IdP A on port 8080; waits for its discovery document.
- Logs in as
aliceat IdP A via the admin API, then callsPOST /api/admin/federated-accountsto linkbob@PARTNER.LOCALtoalice@CORP.LOCAL. - 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:
- Enter
alicein the username field and press Enter. - The page detects the federation hint and redirects to IdP B’s login form.
- Enter
bob/bob123at IdP B. - IdP B redirects back to IdP A’s callback (
/internal/callback/partner-idp). - IdP A maps
bob@PARTNER.LOCALtoalice@CORP.LOCALand issues a local session. - The admin panel shows
aliceas the logged-in user.
Local login path:
- Enter
caroland press Enter. - A password field appears (no federation hint for carol).
- 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
| File | Description |
|---|---|
idpa.toml | IdP A base config; __IDPA_CLIENT_ID__ placeholder substituted at runtime |
idpb.toml | IdP B config; registration_token = "demo-federation-setup-token" enables POST /register |
users-idpa.toml | Local users for IdP A (alice, carol) |
users-idpb.toml | Local 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
- Federation — full federation configuration reference.
- GitHub federation demo — using GitHub as an upstream OAuth2 provider.