SAML2 Federation
Ahdapa supports the SAML2 protocol in two roles:
- Identity Provider (IdP): Issue SAML2 assertions to Service Providers that require SAML SSO (legacy enterprise apps, SaaS platforms).
- Service Provider (SP): Consume SAML2 assertions from upstream Identity Providers (ADFS, Okta, Shibboleth) as an alternative to OIDC federation.
Both roles are disabled by default. Enable them in the [saml2] configuration
section:
[saml2]
idp_enabled = true
sp_enabled = true
See Configuration Reference – [saml2] for the full
list of keys.
IdP Role
When idp_enabled = true, Ahdapa publishes IdP metadata at /saml2/metadata
and accepts AuthnRequests at /saml2/sso.
Metadata
GET /saml2/metadata returns an XML EntityDescriptor containing:
- The IdP entity ID (derived from the server issuer URL)
SingleSignOnServiceendpoints for HTTP-Redirect and HTTP-POST bindingsSingleLogoutServiceendpoints for HTTP-Redirect and HTTP-POST bindings at/saml2/sloArtifactResolutionServiceendpoint at/saml2/artifact(SOAP binding)- A
KeyDescriptorwith the signing certificate (generated from the active JWT signing key)
Register this URL with Service Providers that need to discover Ahdapa’s SAML endpoints.
SSO Flow
- The SP sends an
AuthnRequestto/saml2/ssovia HTTP-Redirect (GET with DEFLATE+base64-encoded query parameter) or HTTP-POST (POST with base64-encoded form field). - If the user has an existing Ahdapa session cookie (from any prior authentication – OAuth2, Kerberos, password, passkey), the IdP builds a SAML Response directly from the session claims.
- If no session exists, the pending AuthnRequest is stored in the
pending_saml2_authdatabase table and the user is redirected to the login page. After login, the browser returns to/saml2/sso/continueto complete the flow. - When
sign_responsesorsign_assertionsis enabled (both default totrue), the SAML Response is signed via libxmlsec1 using the server’s active JWT signing key. - The SAML Response is delivered to the SP’s Assertion Consumer Service URL
via an auto-submitting HTML form (HTTP-POST binding), or via Artifact
binding when the SP’s
preferred_bindingis set to HTTP-Artifact.
Assertion construction from SessionClaims:
| SAML Element | Source |
|---|---|
Issuer | Server issuer URL |
Subject/NameID | SessionClaims.sub |
Conditions/AudienceRestriction | SP’s entity ID |
Conditions/NotBefore/NotOnOrAfter | Current time +/- clock skew |
AuthnStatement/AuthnContextClassRef | SessionClaims.acr (already uses SAML2 URNs) |
AttributeStatement | User attributes from session (groups, email) |
NameID formats supported: persistent, transient, email, kerberos, unspecified.
Registering Service Providers
SPs are registered via the admin API. See Admin API – SAML2 Service Providers.
curl -s -b session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"entity_id": "https://sp.example.com/saml2/metadata",
"acs_urls": ["https://sp.example.com/saml2/acs"],
"name_id_format": "persistent",
"want_assertions_signed": false
}' \
https://idp.example.com/api/admin/saml2/sp
SPs can also be imported from metadata:
curl -s -b session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{"metadata_url": "https://sp.example.com/saml2/metadata"}' \
https://idp.example.com/api/admin/saml2/sp/import-metadata
IdP-Initiated (Unsolicited) SSO
POST /saml2/sso/initiate starts an SSO flow without a prior AuthnRequest.
The user must have an active session. Form parameters:
| Parameter | Required | Description |
|---|---|---|
sp_entity_id | yes | Target SP entity ID |
relay_state | no | Opaque value forwarded to the SP |
name_id_format | no | NameID format URI override |
The response has no InResponseTo attribute (unsolicited).
Assertion Encryption
When an SP has an encryption certificate registered, assertions are
automatically encrypted using AES-256-GCM (content encryption) with an
RSA-OAEP wrapped session key. Encryption happens after signing, following the
SAML2 sign-then-encrypt ordering (SAML2 Core section 6.2). The SP receives an
<EncryptedAssertion> element containing the <xenc:EncryptedData>.
Register the encryption certificate via the admin API:
curl -s -b session.jar \
-X PUT -H 'Content-Type: application/json' \
-d '{"encryption_certificate_pem": "-----BEGIN CERTIFICATE-----\n..."}' \
https://idp.example.com/api/admin/saml2/sp/https%3A%2F%2Fsp.example.com
HBAC for SAML2 SSO
When HBAC rules are configured, the SAML2 IdP evaluates access control before
issuing assertions. The SP entity ID is used as the client_id in the HBAC
context, and the user’s group memberships are resolved via the standard cascade
(static users, varlink, IPA). If the request is denied, the IdP returns a SAML
error response with urn:...:status:RequestDenied.
SP Role
When sp_enabled = true, Ahdapa can consume SAML2 assertions from upstream
Identity Providers.
Registering Upstream IdPs
Upstream SAML IdPs can be registered via the admin API or pre-configured in TOML. See Admin API – SAML2 Upstream Identity Providers.
Admin API:
curl -s -b session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"entity_id": "https://adfs.corp.example.com/adfs/services/trust",
"local_id": "corp-adfs",
"sso_url": "https://adfs.corp.example.com/adfs/ls/",
"preferred_binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
"default_groups": ["corp-users"]
}' \
https://idp.example.com/api/admin/saml2/idp
Metadata import (alternative to manual registration):
curl -s -b session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"metadata_url": "https://adfs.corp.example.com/FederationMetadata/2007-06/FederationMetadata.xml",
"local_id": "corp-adfs"
}' \
https://idp.example.com/api/admin/saml2/idp/import-metadata
TOML configuration (seeded at startup, admin API updates take precedence):
[[saml2.upstream_idps]]
local_id = "corp-adfs"
entity_id = "https://adfs.corp.example.com/adfs/services/trust"
sso_url = "https://adfs.corp.example.com/adfs/ls/"
preferred_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
default_groups = ["corp-users"]
certificates_pem = ["""
-----BEGIN CERTIFICATE-----
MIICxjCC...
-----END CERTIFICATE-----
"""]
The local_id is the URL slug used in the SP auth endpoint. It must contain
only alphanumeric characters, dashes, or underscores.
SP Authentication Flow
- The browser is directed to
/saml2/auth/{local_id}(wherelocal_idmatches the registered upstream IdP). - Ahdapa generates a SAML
AuthnRequestand redirects the browser to the upstream IdP’s SSO URL. - The user authenticates at the upstream IdP.
- The upstream IdP sends a SAML Response to
/saml2/acs/{local_id}via HTTP-POST. - The ACS handler extracts the NameID and attributes from the assertion,
creates a
SessionClaimscookie, and resumes any pending OAuth2 authorization flow.
SP Metadata
Each registered upstream IdP gets a dedicated SP metadata endpoint at
/saml2/sp/metadata/{local_id}. Register this URL at the upstream IdP so it
can discover Ahdapa’s ACS URL and entity ID.
The SP entity ID is the server’s issuer URL ({issuer}). All upstream IdPs
share the same SP entity ID; the local_id differentiates them via the ACS
URL path (/saml2/acs/{local_id}).
Attribute Mapping
When consuming assertions from upstream IdPs, the attribute_map field on the
IdP entry maps SAML attribute names (or friendly names) to internal claim
names. For example, to map a custom group attribute:
{
"attribute_map": {
"groups": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
}
}
The groups key is special: when set, it overrides the default group attribute
lookup (which searches for OID urn:oid:1.3.6.1.4.1.5923.1.5.1.1 and
friendly name isMemberOf).
Single Logout (SLO)
The /saml2/slo endpoint handles front-channel Single Logout for both IdP and
SP roles, via HTTP-Redirect (GET) and HTTP-POST (POST).
Receiving a LogoutRequest:
- Parse the incoming
LogoutRequest(DEFLATE+base64 for Redirect, base64 for POST). - Revoke the user’s session via the CRDT
revoked_sessionsmechanism (same as OIDC back-channel logout). - Cascading logout: propagate the logout to all other SPs that received assertions during this session. For each participant SP that has an SLO URL, a LogoutRequest is sent via SOAP back-channel.
- Look up the requestor’s SLO return URL from the SP or upstream IdP CRDT entries.
- Send a
LogoutResponsevia HTTP-POST auto-submit form.
Receiving a LogoutResponse: logged for audit purposes.
Session participants are tracked in the saml2_session_participants database
table. Each time an assertion is delivered to an SP, the session index and SP
entity ID are recorded so the SLO handler knows which SPs to notify.
SP-initiated Single Logout
The /saml2/slo/start/{upstream_id} endpoint initiates a Single Logout flow
from the SP side. When a user with an active session visits this endpoint,
Ahdapa:
- Revokes the local session via the CRDT
revoked_sessionsmechanism. - Sends a
LogoutRequestto the upstream IdP’s SLO URL via the upstream’s preferred binding (HTTP-Redirect or HTTP-POST).
This endpoint requires an active session cookie and the upstream IdP must have
a slo_url configured.
Artifact Binding
The IdP role supports the SAML2 Artifact binding (type 0x0004) in addition to
HTTP-POST. When an SP’s preferred_binding is set to
urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact, the SSO handler:
- Serializes the SAML Response as usual.
- Generates a SAML artifact (type code 0x0004) containing the IdP entity ID hash and a random message handle.
- Stores the Response XML and message handle in the
saml2_artifactsdatabase table (60-second TTL). - Redirects the browser to the SP’s ACS URL with the artifact as a query
parameter (
?SAMLart=...). - The SP resolves the artifact by sending a SOAP
ArtifactResolverequest toPOST /saml2/artifact, which returns the original Response XML inside a SOAPArtifactResponseenvelope.
The IdP metadata at /saml2/metadata advertises the artifact resolution
service endpoint.
XML Digital Signatures
Outgoing SAML Responses are signed when sign_responses = true (the
default) or sign_assertions = true. The SSO handler calls the libxmlsec1 FFI
sign_xml function to apply an enveloped XML Digital Signature. Supported key
types for XML signing: RSA (RS256–RS512, PS256–PS512), ECDSA P-256 (ES256),
and ECDSA P-384 (ES384). A self-signed certificate wrapping the signing key is
generated on the fly and included in the KeyInfo element.
By default, SAML signing uses the same key as jwt_signing_algorithm. When the
server uses EdDSA or ML-DSA for JWTs (which xmlsec1 does not support), set
saml2.signing_algorithm to an ECDSA algorithm:
[saml2]
signing_algorithm = "ES256"
This loads a separate ECDSA key for SAML XML signatures while JWT signing continues to use EdDSA/ML-DSA.
Incoming assertion signatures are verified in the ACS handler
(/saml2/acs/{upstream_id}) and the SLO handler (/saml2/slo) using the
upstream IdP’s or SP’s registered certificates. If the upstream IdP has no
certificates configured, the ACS handler rejects the response. Similarly, the
SLO handler rejects LogoutRequests from requestors with no certificates.
Content Security Policy for auto-submit forms
SAML2 POST binding responses use auto-submitting HTML forms
(document.forms[0].submit()). These forms are served with a dedicated
per-response Content Security Policy that whitelists only the SHA-256 hash of
the inline submit script:
script-src 'sha256-8lDeP0UDwCO6/RhblgeH/ctdBzjVpJxrXizsnIk3cEQ='
This is separate from the WebUI’s CSP (which dynamically computes hashes for
all inline scripts in index.html).
RequestedAuthnContext Comparison
The IdP evaluates RequestedAuthnContext in incoming AuthnRequests using all
four SAML2 Core comparison modes: exact, minimum, maximum, and better.
The minimum, maximum, and better modes require an ordered ranking of
authentication context class references. Configure the ranking in acr_order
(weakest first):
[saml2]
acr_order = [
"urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
"urn:oasis:names:tc:SAML:2.0:ac:classes:Password",
"urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken",
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract",
"urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos",
]
The default order is shown above. ACR values not present in the list have no
rank — ordered comparisons fall back to exact matching, and better fails
closed (no match).
Known Limitations
-
Front-channel cascading SLO not supported. When the IdP receives a LogoutRequest, it propagates logout to other session participant SPs via SOAP back-channel only. Browser redirect chain (front-channel) cascading is not implemented. SPs that only support HTTP-Redirect or HTTP-POST SLO will not receive cascading logout notifications.
-
EdDSA and ML-DSA XML signatures not supported natively. The xmlsec1 FFI supports RSA and ECDSA key types only. Set
saml2.signing_algorithm(e.g."ES256") to use an ECDSA key for SAML signing when the server’sjwt_signing_algorithmis EdDSA or ML-DSA.