OIDC Integration

wcs.backend integrates OpenID Connect (OIDC) single sign-on so a Webcloud7 CMS site can authenticate users against an external OIDC provider (for example Keycloak). The OIDC protocol handling is provided by the external pas.plugins.oidc package (a hard dependency declared in setup.py); wcs.backend overrides the callback and logout views to add token handling, redirect-host validation and optional REST API token issuance.

Architecture Overview

OIDC is implemented as a PAS (Pluggable Authentication Service) plugin from pas.plugins.oidc, installed in the site’s acl_users. wcs.backend replaces two of the plugin’s browser views with its own implementations (login/oidc.py):

┌──────────────┐  auth request   ┌──────────────┐
│   Browser    │ ───────────────▶│   OIDC IdP   │
│              │ ◀───────────────│  (Keycloak,  │
└──────────────┘  code + state   │     …)       │
       │                         └──────────────┘
       │ /callback?code=…&state=…
       ▼
┌─────────────────────────┐     ┌─────────────────┐
│  CallbackView           │────▶│   Plone PAS     │
│  (wcs.backend override)  │     │  (acl_users)    │
└─────────────────────────┘     └─────────────────┘
       │ rememberIdentity
       ▼
┌─────────────────────────┐
│  User session +          │
│  optional API token      │
└─────────────────────────┘

The two overridden views (registered on the OIDC plugin, IOIDCPlugin) are:

  1. CallbackView — handles the redirect back from the IdP: token exchange, user info retrieval, identity remembering, and the post-login redirect.

  2. LogoutView — builds the end-session request to the IdP and expires the local session and API token cookies.

Connection details (provider URL, client id/secret, scopes, redirect URIs, the create_restapi_ticket flag, etc.) are configured on the pas.plugins.oidc plugin itself in acl_users. The settings below are the wcs.backend-specific registry records that tune the overridden views.

Configuration

The following registry records are installed by wcs.backend (profile registry/oidc.xml). They control the behavior of the overridden callback and logout views.

Record

Type

Default

Description

wcs.backend.oidc.use_access_token

Bool

False

When True, call the userinfo endpoint with a Bearer access token (client_secret_basic token request) instead of reading user info from the ID token / response body.

wcs.backend.oidc.include_api_token

Bool

False

When True, append the REST API JWT (auth_token) to the post-login redirect URL. Only applies if the plugin’s create_restapi_ticket property is enabled.

wcs.backend.oidc.allowed_hosts

List

['localhost']

Hostnames that are allowed as redirect targets after login and after logout. A came_from / redirect_uri whose host is not in this list falls back to the site root.

These records can be edited through the registry control panel (/@@registry) or shipped via a registry profile.

Login Flow

From an integrator’s perspective:

  1. An anonymous visitor is sent to the OIDC plugin’s login view (provided by pas.plugins.oidc), which redirects to the IdP’s authorization endpoint. The original location is tracked as came_from in the session.

  2. The user authenticates at the IdP.

  3. The IdP redirects back to the plugin’s callback view (overridden by wcs.backend’s CallbackView):

    • the authorization response is parsed and the token is exchanged;

    • user info is fetched — from the userinfo endpoint with a Bearer token when use_access_token is enabled, otherwise from the standard response;

    • rememberIdentity creates/updates the Plone user and establishes the session;

    • the browser is redirected back.

  4. The redirect target is the came_from URL when its host is in wcs.backend.oidc.allowed_hosts; otherwise the site root. When include_api_token is enabled and the plugin issues a REST ticket, the JWT is appended as ?auth_token=…&oidc_login=1.

Logout

The overridden logout view:

  • builds an OIDC EndSessionRequest to the IdP (using post_logout_redirect_uri + client_id, or the deprecated redirect_uri form when the plugin’s use_deprecated_redirect_uri_for_logout property is set);

  • expires the Plone auth cookie and the auth_token cookie locally;

  • redirects to the IdP end-session endpoint.

The post-logout redirect_uri request parameter is honored only when its host is listed in wcs.backend.oidc.allowed_hosts; otherwise the site root is used.

Identity Provider Setup (Keycloak example)

When using Keycloak as the OIDC provider, create an openid-connect client and register the redirect URIs that match the plugin’s callback and logout views, for example:

  • Valid redirect URI: https://your-site.com/acl_users/<plugin-id>/callback

  • Valid post-logout redirect URI: https://your-site.com (and any host listed in allowed_hosts)

The exact plugin id and view paths come from the installed pas.plugins.oidc plugin; register that plugin’s redirect URIs in the IdP client.

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, funnelling visitors into the configured SSO login. 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 OIDC plugin views live under acl_users, so the login/callback 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 plugin’s views (under acl_users) and the login form are explicitly allow-listed, force-login and OIDC work together: the public site is locked down while the OIDC handshake and the REST API remain reachable.

Using the API Token after Login

When wcs.backend.oidc.include_api_token is enabled (and the OIDC plugin issues a REST ticket), the post-login redirect carries the JWT in the URL. A frontend can read it and use it as a Bearer token for subsequent REST API calls:

// After the OIDC redirect lands on your SPA:
const params = new URLSearchParams(window.location.search);
const token = params.get('auth_token');

if (params.get('oidc_login') === '1' && token) {
    // Use the token for authenticated REST API requests
    const response = await fetch('https://your-site.com/++api++/@navigation', {
        headers: {
            'Accept': 'application/json',
            'Authorization': `Bearer ${token}`,
        },
    });
    const data = await response.json();
}