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. - 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 withreturn_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.
Demo accounts
Passwords are generated randomly at startup and printed to the console.
| Username | IdP | What happens on login at IdP A |
|---|---|---|
| alice | A (local) | Redirect to IdP B; log in as bob; redirected back as alice |
| carol | A (local) | Local password login; no federation redirect |
| bob | B (local) | Direct login at IdP B only |
| diana | 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
# 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
- 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). - If Kerberos tools are installed, starts an ephemeral MIT KDC for realm
CORP.LOCALwithalice@CORP.LOCALandHTTP/localhost@CORP.LOCALprincipals. - 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_idand keytab path into the template. - 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. - Runs automated tests:
- Drives a scripted federated login (alice → IdP B → bob → callback).
- Verifies
upstream-token.depositedaudit event in idpa.log. - If KDC is available: kinit + SPNEGO retrieval of the stored upstream token.
- Prints PASS/FAIL summary.
- 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
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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:
- Enter
alicein the username field and press Enter. - The page detects the federation hint and redirects to IdP B’s login form.
- Enter
boband the password printed at startup at 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 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.
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.in | Users template for IdP A (alice, carol); passwords substituted at runtime |
users-idpb.toml.in | Users template for IdP B (bob, diana); passwords substituted at runtime |
clients-idpa.toml | Static 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
- Federation — full federation configuration reference.
- GitHub federation demo — using GitHub as an upstream OAuth2 provider.