Skip to content

Web Services: Authentication ​

Note

Development of the Dialogue Branch Platform (including the Web Service) occurs on the main branch of the monorepo. For stable software versions, check out the release tags.

The Dialogue Branch Web Service is a pure OAuth2 resource server: it validates Bearer tokens issued by Keycloak, an open source Identity and Access Management platform, but plays no role in issuing, refreshing, or otherwise managing tokens itself. There is no /auth/login end-point, and no built-in username/password store — all user management happens in Keycloak.

There are two distinct ways a client ends up presenting a valid token to the Web Service, depending on what kind of client it is:

  • A direct API client (a custom integration, a script, or the bundled Swagger UI) performs the OAuth2 flow itself and attaches the resulting access token to every request — see Direct API Clients below.
  • Dialogue Branch Studio (apps/studio), the bundled reference client, never performs the OAuth2 flow itself and never holds a token at all. It delegates authentication entirely to a separate Backend-for-Frontend service (apps/bff) that sits in front of the Web Service — see Studio Authentication (via the BFF) below.

Direct API Clients ​

A client application authenticates a user directly with Keycloak, using the Authorization Code + PKCE flow, against the realm and client configured on the Web Service deployment (by default, realm dialoguebranch, client id dlb-web-service, a public client). Once Keycloak returns an access token, the client includes it on every call to the Web Service:

text
Authorization: Bearer <access-token>

The Web Service validates this token by fetching Keycloak's JSON Web Key Set (JWKS) at {keycloak.baseUrl}realms/{keycloak.realm}/protocol/openid-connect/certs and verifying the token's signature, issuer and expiry — it never contacts Keycloak's token end-point itself. On top of that, it checks the token's azp (authorized-party) claim against the list of client IDs in dlb.auth.keycloak.trusted-clients (environment variable DLB_AUTH_KEYCLOAK_TRUSTED_CLIENTS): being signed by a trusted realm is not enough, the token must also have been issued to a client this deployment trusts. Left unset, the list defaults to the deployment's own client-id plus the BFF's client id (dlb-bff) — the BFF forwards tokens issued to its own client, and Dialogue Branch Studio reaches the Web Service only through the BFF, so this default covers the standard setup with no configuration. In a shared Keycloak realm where further back-end services authenticate against the same realm, set the property to a comma-separated list of their client IDs, or to a single * to accept a token from any client in a trusted realm. An explicit list replaces the default entirely, so a BFF-fronted deployment that sets it must still include dlb-bff. A token from an untrusted client is rejected with 401, and the first rejection of each distinct azp is logged at WARN. The Swagger UI bundled with the Web Service is itself configured as an OAuth2 client (with PKCE) against the same Keycloak realm, so you can authenticate directly from the "Authorize" button on the Swagger page without writing any client code — see Tutorial: Web Service - Exploring the API.

Studio Authentication (via the BFF) ​

apps/bff is a small Spring Boot Backend-for-Frontend service that sits between Dialogue Branch Studio (apps/studio) and the Web Service. It exists so that the browser never has to hold an OAuth2 access or refresh token: the token lives server-side, in the BFF's HTTP session, and the browser only ever holds a JSESSIONID session cookie.

The flow:

  1. Studio sends the browser to the BFF's GET /login end-point (a real top-level navigation, not a fetch/XHR call), a generic login trigger that doesn't presuppose anything about how the BFF is configured. The BFF redirects to its own actual OAuth2 authorization endpoint, which performs the Authorization Code + PKCE exchange against Keycloak itself and stores the resulting access/refresh token in the browser's HTTP session.
  2. After login, Studio calls GET /whoami on the BFF to learn who's logged in. The BFF decodes the session's access token server-side and returns { "username": ..., "roles": [...] } — the same preferred_username and resource_access → <client-id> → roles claims described under Roles below, just read out on the server instead of in the browser.
  3. Every Web Service call Studio makes goes to the BFF's /api/** end-point instead of the Web Service directly. The BFF proxies the request through, attaching the session's access token as the Authorization: Bearer header itself. GET /api/v1/info/all is the one path the BFF forwards without a session at all, matching the Web Service's own public /info/all end-point (used by Studio's pre-login reachability check).
  4. Token refresh happens transparently inside the BFF (via Spring Security's OAuth2AuthorizedClientManager) — Studio never sees or handles a refresh token.
  5. Logging out is a POST /logout to the BFF (a real form submission, not a fetch — required both to satisfy Spring Security's default LogoutFilter matcher and so cookies survive the redirect to Keycloak's own logout page). This is RP-initiated logout: it ends both the BFF's session and the underlying Keycloak SSO session.
  6. State-changing requests (anything but GET/HEAD/OPTIONS/TRACE) must carry an X-XSRF-TOKEN header matching the XSRF-TOKEN cookie the BFF sets — the standard CSRF protection recipe for cookie-authenticated SPAs.

Because the BFF only relays Bearer tokens it already obtained, the Web Service itself is completely unaware of its existence — from the Web Service's point of view, calls arriving via the BFF look identical to calls from any other direct API client.

Roles ​

Once a token is validated, the Web Service reads the caller's username from the JWT's preferred_username claim, and their roles from the JWT's resource_access → <client-id> → roles claim, where <client-id> is the deployment's configured dlb.auth.keycloak.client-id (dlb-web-service by default) — i.e. client roles configured on that client in Keycloak (not Keycloak realm roles). Four roles are recognised:

  • participant — may execute dialogues (/dialogue/*) and read/write their own Dialogue Branch Variables (/variables/*).
  • editor — everything participant can do, plus authoring and publishing dialogue content (/authoring/*, /publish/*, /test-session/*) and listing dialogues in a project (/dialogue/list-dialogues).
  • admin — everything editor can do, plus managing projects (/project/*), reading technical service information (/info/technical), and acting on behalf of another user by passing the optional delegateUser parameter present on most end-points (see Acting on behalf of another user).
  • supervisor — everything participant can do, plus reading another user's data by passing delegateUser on the read-only end-points: their variables (/variables/get), variable history (/variables/history) and dialogue logs (/log/get-session, /log/verify-id). A supervisor can never act on another user's behalf (run dialogues or set variables for them), and has no authoring or administration rights. Typical holders are users who follow other users' progress, such as caregivers, coaches or teachers. Which users a supervisor may read is not restricted beyond the caller's own realm; a deployment that needs finer rules enforces them in front of the Web Service.

Internally each role maps to a fixed set of permissions, and every end-point requires one specific permission. A caller whose token is valid but whose roles do not grant the required permission gets a 403 Forbidden response with error code INSUFFICIENT_PRIVILEGES. (A missing, expired, or otherwise invalid token is rejected earlier, with 401 Unauthorized.)

Acting on behalf of another user ​

An admin can pass delegateUser (the other user's subject, within the admin's own realm) to act on that user's behalf. This is a powerful operation, meant for testing and support:

  1. Testing with a test user, for example one without any variables yet. Changing that user's data is the point.
  2. Investigating a real user's problem, by going through dialogues as they would. Here nothing of theirs should change, so simulate the user instead (see below).
  3. Deliberately fixing a user's data, for example setting a variable so that a dialogue stops being offered to them.

Acting on someone's behalf for real, on /dialogue/* and /variables/set*, changes that user's real data:

  • the dialogues run, and the variables changed, become part of their own dialogue logs, variables and variable history, indistinguishable from their own actions;
  • a configured External Variable Service is told about the changes as if the user made them;
  • it can interfere with a dialogue the user has ongoing themselves.

Their time zone is left alone: on a delegated write the timeZone parameter is ignored (see Known Users).

Two ways to use delegateUser change nothing of the other user's:

  • Reading their data: /variables/get, /variables/history, /log/get-session and /log/verify-id. This needs only the USER_DELEGATE_READ permission, which the supervisor role also has.
  • Simulating them: a test session (/test-session/*, see the Authoring API) started with delegateUser runs against a copy of their variables. Their stored variables are only read, to seed the copy, and nothing is written back. Dialogue Branch Studio offers this as Simulate as.

A backend that serves many end users should not use one admin account to act for all of them. Instead, it lets each user authenticate and forwards that user's own access token to the Web Service, as Dialogue Branch Studio's BFF does: each request is then made by the user it is about, with that user's own role.

Public End-Points ​

A small number of Web Service end-points do not require a token at all:

  • /info/all — basic service information (build time, protocol version, service version, uptime).
  • /swagger-ui/**, /v3/api-docs/** — the Swagger UI and its OpenAPI document.
  • /actuator/health, /actuator/info — Spring Boot Actuator health/info endpoints.

Every other Web Service end-point requires a valid Bearer token. The BFF mirrors this for the one path Studio needs before login: its own GET /api/v1/info/all and /actuator/health/** are reachable without a session; every other BFF end-point (/api/**, /whoami) requires one, returning a plain 401 (rather than redirecting to Keycloak) for unauthenticated fetch/XHR calls.

Local Development ​

When running the platform's local development stack (see the repository's top-level infrastructure/docker/compose.yml), Keycloak is available at http://localhost:8081. The bundled realm import does not seed any demo users — after starting the stack, log in to the Keycloak admin console (default credentials admin/admin) and create a test user, assigning it one or more of the participant/editor/admin client roles on the dlb-web-service client, before you can call any authenticated Web Service end-point.

Running docker compose --profile api up also builds and starts the BFF (http://localhost:8082) alongside the Web Service, since developing Studio locally needs both. Studio's Vite dev server proxies /api, /oauth2, /login, /logout, /whoami, and /actuator to the BFF (see apps/studio/vite.config.js), so during local development you can treat the dev server's own origin (http://localhost:5173) as if it served these paths directly.