# 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`): ```text ┌──────────────┐ 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//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: ```javascript // 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(); } ```