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:
SamlAuthPlugin(PAS) — holds all SP/IdP configuration as plugin properties, implementsIChallengePlugin, and turns a validated SAML assertion into a Plone user and session (remember_identity).Protocol views —
sls(start login),acs(assertion consumer),slo/logout(single logout),metadata(SP metadata), and theidp_metadataadmin form for importing IdP metadata.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 a Plone session ( |
|
|
Issue a JWT and set the |
|
|
Append the JWT as an |
|
|
Create a matching Plone user on first login if one does not already exist. |
|
|
Store the outgoing |
|
|
Extra hostnames (beyond the site’s own host) that |
|
|
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 |
|---|---|
|
Service Provider block ( |
|
Identity Provider block ( |
|
Security and metadata options ( |
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 intosettings_idp.
Identity Provider Setup (Keycloak example)¶
When using Keycloak as the IdP, create a SAML client in the realm:
Create Client
Clients → Create → Client type:
SAMLClient ID: the SP
entityId, i.e.https://your-site.com/acl_users/<plugin-id>/metadata
Configure endpoints
Valid redirect URIs / Assertion Consumer Service POST URL:
https://your-site.com/acl_users/<plugin-id>/acsLogout Service Redirect Binding URL:
https://your-site.com/acl_users/<plugin-id>/slo
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).
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:
An anonymous visitor requests a protected page. The force-login behavior raises
Unauthorized, which the plugin’schallengeturns into a redirect to…/<plugin-id>/require_login?came_from=<original-url>.require_loginredirects to the plugin’sslsview (the login entry point), which builds a SAMLAuthnRequestand redirects the browser to the IdP. The original URL is carried asRelayState.The user authenticates at the IdP.
The IdP POSTs the signed SAML response to the
acs(Assertion Consumer Service) view. The plugin validates the assertion, then callsremember_identity:if
create_useris 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.
The visitor is redirected back. The target is the
RelayStateURL when its host is the site’s own host or appears inallowed_redirect_hosts; otherwise the site root. Wheninclude_api_token_in_redirectandcreate_api_sessionare 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 |
|---|---|
|
Start login — builds the |
|
Assertion Consumer Service — receives and validates the SAML response. |
|
Single Logout — handles IdP-initiated logout requests. |
|
SP-initiated logout — builds a |
|
Returns the SP metadata XML for registration at the IdP. |
|
Redirect target of the challenge plugin; sends the user to |
|
Admin form to import IdP metadata into the plugin. |
Single Logout¶
logout(SP-initiated) builds a SAMLLogoutRequestand redirects to the IdP’s single logout endpoint.slo(IdP-initiated) processes an incomingLogoutRequest, 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
Acceptheader isapplication/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, oracl_users(the SAML protocol views live underacl_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.