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

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)
  • SingleSignOnService endpoints for HTTP-Redirect and HTTP-POST bindings
  • SingleLogoutService endpoints for HTTP-Redirect and HTTP-POST bindings at /saml2/slo
  • ArtifactResolutionService endpoint at /saml2/artifact (SOAP binding)
  • A KeyDescriptor with 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

  1. The SP sends an AuthnRequest to /saml2/sso via HTTP-Redirect (GET with DEFLATE+base64-encoded query parameter) or HTTP-POST (POST with base64-encoded form field).
  2. 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.
  3. If no session exists, the pending AuthnRequest is stored in the pending_saml2_auth database table and the user is redirected to the login page. After login, the browser returns to /saml2/sso/continue to complete the flow.
  4. When sign_responses or sign_assertions is enabled (both default to true), the SAML Response is signed via libxmlsec1 using the server’s active JWT signing key.
  5. 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_binding is set to HTTP-Artifact.

Assertion construction from SessionClaims:

SAML ElementSource
IssuerServer issuer URL
Subject/NameIDSessionClaims.sub
Conditions/AudienceRestrictionSP’s entity ID
Conditions/NotBefore/NotOnOrAfterCurrent time +/- clock skew
AuthnStatement/AuthnContextClassRefSessionClaims.acr (already uses SAML2 URNs)
AttributeStatementUser 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:

ParameterRequiredDescription
sp_entity_idyesTarget SP entity ID
relay_statenoOpaque value forwarded to the SP
name_id_formatnoNameID 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

  1. The browser is directed to /saml2/auth/{local_id} (where local_id matches the registered upstream IdP).
  2. Ahdapa generates a SAML AuthnRequest and redirects the browser to the upstream IdP’s SSO URL.
  3. The user authenticates at the upstream IdP.
  4. The upstream IdP sends a SAML Response to /saml2/acs/{local_id} via HTTP-POST.
  5. The ACS handler extracts the NameID and attributes from the assertion, creates a SessionClaims cookie, 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:

  1. Parse the incoming LogoutRequest (DEFLATE+base64 for Redirect, base64 for POST).
  2. Revoke the user’s session via the CRDT revoked_sessions mechanism (same as OIDC back-channel logout).
  3. 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.
  4. Look up the requestor’s SLO return URL from the SP or upstream IdP CRDT entries.
  5. Send a LogoutResponse via 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:

  1. Revokes the local session via the CRDT revoked_sessions mechanism.
  2. Sends a LogoutRequest to 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:

  1. Serializes the SAML Response as usual.
  2. Generates a SAML artifact (type code 0x0004) containing the IdP entity ID hash and a random message handle.
  3. Stores the Response XML and message handle in the saml2_artifacts database table (60-second TTL).
  4. Redirects the browser to the SP’s ACS URL with the artifact as a query parameter (?SAMLart=...).
  5. The SP resolves the artifact by sending a SOAP ArtifactResolve request to POST /saml2/artifact, which returns the original Response XML inside a SOAP ArtifactResponse envelope.

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’s jwt_signing_algorithm is EdDSA or ML-DSA.