SAML Integration

wcs.backend integrates SAML 2.0 single sign-on so a Webcloud7 CMS site can act as a SAML Service Provider (SP) against an external Identity Provider (IdP) such as Keycloak, ADFS or OneLogin. Users authenticate at the IdP and are transparently created and logged in on the Plone side.

The SAML functionality itself is provided by the external wcs.samlauth package (a hard dependency declared in setup.py). wcs.backend pulls it in and wires the overall login experience around it — the shared login form, the force-login behavior that protects the site from anonymous access, and the user-area provisioning that runs after a successful login.

Architecture Overview

The SAML login is implemented as a PAS (Pluggable Authentication Service) plugin installed in the site’s acl_users. The plugin exposes a set of browser views that speak the SAML protocol with the IdP, and acts as a IChallengePlugin so unauthorized requests are redirected into the SAML flow.

┌──────────────┐   AuthnRequest    ┌──────────────┐
│   Browser    │ ────────────────▶ │     IdP      │
│              │ ◀──────────────── │  (Keycloak,  │
└──────────────┘   SAML Response   │   ADFS, …)   │
       │                           └──────────────┘
       │ POST assertion (/acs)
       ▼
┌─────────────────────┐     ┌─────────────────┐
│  SamlAuthPlugin     │────▶│   Plone PAS     │
│  (acs / sls / slo)  │     │  (acl_users)    │
└─────────────────────┘     └─────────────────┘
       │ remember_identity
       ▼
┌─────────────────────┐
│  User created /      │
│  session + JWT set   │
└─────────────────────┘

Main moving parts:

  1. SamlAuthPlugin (PAS) — holds all SP/IdP configuration as plugin properties, implements IChallengePlugin, and turns a validated SAML assertion into a Plone user and session (remember_identity).

  2. Protocol viewssls (start login), acs (assertion consumer), slo / logout (single logout), metadata (SP metadata), and the idp_metadata admin form for importing IdP metadata.

  3. Force-login behavior (wcs.backend) — rejects anonymous traversal so visitors are funnelled into the SAML challenge.

Configuration

All SAML settings live as plugin properties on the SamlAuthPlugin in acl_users. After adding the plugin via the ZMI (/acl_users/manage_main), open its properties form to edit them.

Behavior Settings

Property

Default

Description

create_session

True

Create a Plone session (__ac cookie) on successful login.

create_api_session

False

Issue a JWT and set the auth_token cookie for REST API access.

include_api_token_in_redirect

False

Append the JWT as an auth_token query parameter on the post-login redirect (requires create_api_session).

create_user

True

Create a matching Plone user on first login if one does not already exist.

validate_authn_request

False

Store the outgoing AuthnRequest id in the __saml cookie and validate it on the response (InResponseTo).

allowed_redirect_hosts

()

Extra hostnames (beyond the site’s own host) that RelayState is allowed to redirect to after login.

adfs_as_idp

False

Enable ADFS-compatible lowercase URL-encoding of SAML messages.

SAML Settings Documents

The SAML toolkit configuration is split across three text properties, each holding a JSON document:

Property

Description

settings_sp

Service Provider block (sp). entityId, the acs/slo URLs and the SP certificate. The endpoint URLs are filled in automatically from the plugin URL at request time.

settings_idp

Identity Provider block (idp). entityId, single sign-on / single logout endpoints and the IdP signing certificate. Usually populated by importing IdP metadata rather than edited by hand.

advanced

Security and metadata options (strict, debug, signing/encryption requirements, signature/digest algorithms, contact and organization info).

At request time the plugin merges these three documents (advanced first, then sp, then idp) into the settings object handed to the underlying python3-saml toolkit.

Importing IdP Metadata

Instead of editing settings_idp by hand, use the idp_metadata admin form on the plugin (/acl_users/<plugin-id>/idp_metadata). Provide the IdP metadata URL and either:

  • Get Metadata — preview the parsed IdP settings, or

  • Get and store metadata — merge the parsed IdP entityId, SSO/SLO endpoints and certificate into settings_idp.

Identity Provider Setup (Keycloak example)

When using Keycloak as the IdP, create a SAML client in the realm:

  1. Create Client

    • Clients → Create → Client type: SAML

    • Client ID: the SP entityId, i.e. https://your-site.com/acl_users/<plugin-id>/metadata

  2. Configure endpoints

    • Valid redirect URIs / Assertion Consumer Service POST URL: https://your-site.com/acl_users/<plugin-id>/acs

    • Logout Service Redirect Binding URL: https://your-site.com/acl_users/<plugin-id>/slo

  3. Signing

    • Sign assertions / responses as required, and make the IdP signing certificate available so it can be imported into settings_idp (see Importing IdP Metadata).

  4. Attribute / NameID mapping

    • Map the user identifier to the SAML NameID, and add attribute mappers for the user properties you want propagated (e.g. email, full name). Friendly-name attributes are read preferentially; otherwise raw attribute names are used.

Login Flow

From an integrator’s perspective the flow is:

  1. An anonymous visitor requests a protected page. The force-login behavior raises Unauthorized, which the plugin’s challenge turns into a redirect to …/<plugin-id>/require_login?came_from=<original-url>.

  2. require_login redirects to the plugin’s sls view (the login entry point), which builds a SAML AuthnRequest and redirects the browser to the IdP. The original URL is carried as RelayState.

  3. The user authenticates at the IdP.

  4. The IdP POSTs the signed SAML response to the acs (Assertion Consumer Service) view. The plugin validates the assertion, then calls remember_identity:

    • if create_user is enabled and the user is unknown, a Plone user is created and a home folder is provisioned;

    • user properties are updated from the assertion attributes;

    • a Plone session and (optionally) a JWT token are established.

  5. The visitor is redirected back. The target is the RelayState URL when its host is the site’s own host or appears in allowed_redirect_hosts; otherwise the site root. When include_api_token_in_redirect and create_api_session are both enabled, the JWT is appended as ?auth_token=….

Endpoints

All endpoints are views on the plugin object (/acl_users/<plugin-id>/<name>):

View

Purpose

sls

Start login — builds the AuthnRequest and redirects to the IdP.

acs

Assertion Consumer Service — receives and validates the SAML response.

slo

Single Logout — handles IdP-initiated logout requests.

logout

SP-initiated logout — builds a LogoutRequest to the IdP.

metadata

Returns the SP metadata XML for registration at the IdP.

require_login

Redirect target of the challenge plugin; sends the user to sls.

idp_metadata

Admin form to import IdP metadata into the plugin.

Single Logout

  • logout (SP-initiated) builds a SAML LogoutRequest and redirects to the IdP’s single logout endpoint.

  • slo (IdP-initiated) processes an incoming LogoutRequest, clears the Plone session, and finally redirects to …/logged-out.

Force-Login Behavior

wcs.backend registers a IBeforeTraverseEvent subscriber on the site root (login/force_login.py) that rejects anonymous access to most of the site. This is what forces visitors into the SAML challenge.

For each traversal the handler raises Unauthorized for anonymous users unless one of the following applies:

  • the request Accept header is application/json (REST API requests pass through);

  • the request method is OPTIONS (CORS preflight);

  • the URL contains an allowed sub-path — ++resource++, ++theme++, ++plone++static, ++unique++, ++api++, @@site-logo, @@images, @@download, @@display-file, passwordreset, adminauth, or acl_users (the SAML protocol views live under acl_users, so the flow is never blocked);

  • the requested item is an allowed endpoint — favicon.ico, plonejsi18n, login, login_form, require_login, @@login-help, sitemap.xml, sitemap.xml.zg, ok, or @@register.

Because the protocol views and the login form are explicitly allow-listed, force-login and SAML work together: the public site is locked down, while the SAML handshake and the REST API remain reachable.

Testing

SAML integration tests run against a real Keycloak instance configured as a SAML IdP (realm imported from tests/assets/saml-test-realm.json by the Keycloak Docker layer). The test case combines the wcs.backend and wcs.samlauth functional testing layers.

# Run the SAML login tests
bin/test -t test_saml_login

test_saml_login.py covers the end-to-end login (assertion → __ac session → redirect back to the portal) and verifies that a user task container is provisioned in the new user’s home folder after the first SAML login.