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

Federation

Ahdapa can delegate authentication to an external OIDC or OAuth2 identity provider (upstream IdP). When a user’s identity is managed by an upstream IdP — a corporate SSO, a cloud provider, or a FreeIPA-registered external directory — Ahdapa redirects the browser there, receives the callback, maps the returned claims to a local identity, and issues its own OAuth2/OIDC tokens.

The result is a seamless single sign-on flow: end users authenticate at the provider they already use, and applications receive standard Ahdapa tokens.


How the federated login flow works

When an [[federation.upstream_idps]] entry is configured (or auto-discovered from FreeIPA), the login page checks the entered username against GET /api/auth/federated-hint. If the response contains an upstream_id, the browser is immediately redirected to that provider’s authorization endpoint — no local password form is shown.

The hint endpoint resolves an upstream IdP via two steps:

  1. Fast path — looks up the username in the federated_accounts database table (accounts that have previously completed a federated login or were linked by an administrator).
  2. Slow path (IPA only) — when no entry is found and [ipa] gssapi = true, performs an IPA LDAP lookup using the service principal credential (not S4U2Self, because the user has not authenticated yet and cannot read their own ipaidpconfiglink attribute). If ipauserauthtype=idp is set on the IPA user, the endpoint returns {"upstream_id":"ipa-<slug>"} so that first-time IPA IdP users are correctly redirected without needing a prior federated_accounts record.

When configured, a “Sign in with …” button also appears on the login page for users who want to choose their provider explicitly rather than typing an email-matched username.


Manual configuration

Add one [[federation.upstream_idps]] block per upstream IdP in ahdapa.toml:

[[federation.upstream_idps]]
id            = "corp-sso"
issuer        = "https://sso.partner.example.com"
client_id     = "ahdapa"
callback_path = "/internal/callback/corp-sso"
# client_secret = "…"   # omit for public clients (PKCE-only)

The callback_path value must be registered as a redirect URI at the upstream provider. Use the stable cluster hostname (e.g. ipa-ca.<domain>) rather than a per-node FQDN so the URI stays valid across node failures.

Cluster deployments and load balancers: Using a stable cluster hostname for the callback URI is recommended but not strictly required for correctness. Even if the upstream IdP sends the callback to a cluster node that did not start the flow, the handler will forward the browser to the correct originating node via a 302 redirect — see Cross-node callback routing below.

See Configuration Reference — [[federation.upstream_idps]] for the full list of fields including account linkage, scope mapping, and authentication method.


FreeIPA auto-discovery

When [ipa] gssapi = true (the default for IPA co-deployment), Ahdapa automatically reads all ipaIdP LDAP objects from cn=idp,<suffix> at startup and refreshes them every 300 seconds. Each discovered IdP becomes an upstream IdP registration without any TOML entry:

  • id: the CN lowercased with spaces replaced by -, prefixed with ipa- (e.g. "Google Workspace""ipa-google-workspace").
  • callback_path: /internal/callback/ipa-{slug} — register this URL as a redirect URI at the upstream provider. Use the stable ipa-ca.<domain> hostname: https://ipa-ca.ipa.test/idp/internal/callback/ipa-<idp-cn>
  • Authentication method: client_secret_post when ipaIdpClientSecret is present, otherwise none (public client, PKCE-only).
  • The IdP’s issuer is automatically added to the trusted issuers list for JWT validation.

If a static [[federation.upstream_idps]] entry has the same id as an IPA-sourced entry, the static entry takes precedence.

Federated user resolution — no local database row required

When a user whose ipaidpconfiglink points to an IPA-managed IdP completes the federated login flow, Ahdapa resolves the local uid directly via LDAP using the filter:

(&(objectClass=ipaIdpUser)(ipaIdpConfigLink=<dn>)(ipaIdpSub=<external-subject>))

No federated_accounts database row is needed for these users — the link between the IPA uid and the external identity is stored in the IPA directory (ipaIdpConfigLink + ipaIdpSub on the user entry). Only users whose IdP is not registered in FreeIPA require a federated_accounts row created at first login.

LDAP indexes (recommended)

The filter above triggers a full scan of cn=accounts,<suffix> unless equality indexes exist on ipaIdpConfigLink and ipaIdpSub. Add them once on the primary IPA server:

# Replace IPA-EXAMPLE-COM with your realm (dots → dashes).
dsconf slapd-IPA-EXAMPLE-COM backend index add \
    --attr ipaIdpConfigLink --index-type eq userRoot
dsconf slapd-IPA-EXAMPLE-COM backend index add \
    --attr ipaIdpSub --index-type eq userRoot
dsconf slapd-IPA-EXAMPLE-COM backend index reindex \
    --attr ipaIdpConfigLink --attr ipaIdpSub --wait userRoot

The Ansible playbook playbooks/ipa_permissions.yml creates these indexes automatically.


IPA upstream IdP ACR/AMR overrides

IPA-sourced IdPs often do not return acr or amr claims in their userinfo response. To stamp meaningful authentication context into tokens issued after an IPA-managed upstream IdP flow, set per-IdP defaults via the admin panel.

These overrides are stored in the CRDT and gossiped to all cluster nodes, so they survive restarts and take effect on every node without changing the TOML file.

Admin UI

The IPA Upstream IdPs page in the admin panel (/ui/admin/federation/ipa-idps) lists all IdPs currently discovered from cn=idp,<suffix>. Each entry shows the LDAP-sourced fields (issuer, client ID, scopes, callback path) as read-only and exposes two writable fields:

FieldDescription
default_acrACR value stamped when the upstream omits acr in its response. Example: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport".
default_amrAMR values stamped when the upstream omits amr. Comma-separated in the UI; stored as a JSON array. Example: ["pwd", "fed"].

Requires federation:read to view, federation:write to edit.

Admin API

See Admin API — IPA Identity Providers for the full endpoint listing and request/response examples.


Trusted issuers

IPA-sourced IdP issuers are automatically trusted for JWT validation. For manually configured IdPs, list their issuers explicitly:

[federation]
trusted_issuers = [
    "https://sso.partner.example.com",
]

Federation state security

Each time a user is redirected to an upstream IdP, Ahdapa generates a structured state token that is round-tripped through the browser:

base64url(issuer) . base64url(rand32) . base64url(HMAC-SHA256(refresh_key, "ahdapa-fed-state-v1:" || issuer || "." || rand_b64))

The three dot-separated components are:

ComponentContent
base64url(issuer)The canonical issuer URL of the cluster node that started the flow.
base64url(rand32)32 bytes of cryptographically secure randomness (unique per flow).
base64url(MAC)HMAC-SHA256 of the issuer and random components, keyed by the cluster refresh key.

The refresh key is derived from the cluster wrapping key via HKDF (info = "ahdapa-refresh-v1") and is therefore identical on all cluster nodes that share a wrapping key.

When the upstream IdP returns the user to /internal/callback/{id}, the handler verifies the MAC in constant time (using subtle::ConstantTimeEq) before touching the database. A tampered or forged state value is rejected before any redirect or database lookup occurs, preventing open-redirect attacks.

upstream_id CSRF guard: After the MAC is verified and the state row is retrieved from the database, the callback handler confirms that the upstream_id recorded in the database row matches the upstream_id from the URL path. A mismatch (which would indicate tampering with the callback URL) causes the handler to restart the auth flow rather than proceed with the mismatched configuration.

Rolling-upgrade tolerance: state tokens from an older node that did not include the MAC (legacy single-component format, no dots) are detected and handled as an unknown state — the handler restarts the auth flow rather than returning an error, allowing zero-downtime upgrades.


Cross-node callback routing

In a cluster behind a load balancer, the upstream IdP may send the callback to a different node than the one that started the flow. The pending auth state (nonce, PKCE verifier, original request parameters) is stored in the originating node’s local database and is not gossiped.

When the callback handler cannot find the state in its local database, it extracts the originating node’s issuer URL from the verified MAC state token and checks it against the configured gossip.peers and dynamically discovered IPA topology peers. If the issuer belongs to a known cluster member, the handler issues a 302 redirect to forward the browser to the correct node. The originating node completes the normal DB lookup and finishes the flow.

sequenceDiagram
    participant B as Browser
    participant LB as Load Balancer
    participant NA as Node A (started flow)
    participant NB as Node B (received callback)

    B->>LB: GET /internal/callback/corp-sso?code=…&state=…
    LB->>NB: forward
    NB->>NB: verify state MAC — OK
    NB->>NB: DB lookup — not found (flow started on Node A)
    NB->>NB: extract issuer from state → Node A URL
    NB->>NB: check issuer ∈ gossip.peers — known peer
    NB-->>B: 302 → Node A /internal/callback/corp-sso?code=…&state=…
    B->>NA: GET /internal/callback/corp-sso?code=…&state=…
    NA->>NA: verify state MAC — OK
    NA->>NA: DB lookup — found
    NA->>NA: exchange code, create session
    NA-->>B: 302 → resume URL + Set-Cookie: session=…

Security: the forwarding step only occurs after the HMAC is verified. Only known cluster members (listed in gossip.peers or discovered via ipa_topology) are forwarding targets. An unknown or forged issuer in the state token is rejected, and the flow is restarted rather than forwarded.

This mechanism requires no CRDT gossip of pending auth state and adds only one browser round-trip of latency. The upstream IdP callback URI does not need to be node-specific, and using a stable cluster hostname for callback_path remains the recommended practice.


SSRF protection for external URLs

Any administrator-supplied URL that Ahdapa will fetch from — federation callback base URLs, upstream IdP issuer/discovery URLs, OAuth2 client jwks_uri, and SPIFFE bundle endpoint URLs — is validated before storage or use. URLs that could cause server-side request forgery (SSRF) are rejected with 400 Bad Request. The following address ranges are blocked:

CategoryRange / pattern
Non-HTTPSAny http:// URL
Loopback127.0.0.0/8, ::1, localhost, *.localhost
Private IPv410.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
Link-local IPv4169.254.0.0/16
CGNAT / shared address space (RFC 6598)100.64.0.0/10
Network benchmarking (RFC 2544)198.18.0.0/15
Unspecified / broadcast0.0.0.0, 255.255.255.255
IPv6 unique-localfc00::/7
IPv6 link-localfe80::/10
IPv4-mapped IPv6::ffff:0:0/96
IPv6 unspecified / loopback::, ::1

Public HTTPS URLs that do not resolve to any of the above are accepted.


OIDC Federation 1.0

Ahdapa publishes a spec-compliant Entity Statement at /.well-known/openid-federation. Incoming federated assertions are validated against the trusted_issuers list only. Full trust-chain resolution (intermediaries, metadata_policy merging) is not yet implemented.

See Standards for the known-limitations entry.