Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 36 additions & 56 deletions demo-client/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ drives it in a real browser against the running `integration-tests` stack.

The SPA is served *by the gateway itself* as a public asset, so it is same-origin with the reserved
`/auth` paths and exercises the genuine browser-facing contract. The suite runs the same specs
against *both* session modes -- `server` on port 10443 and `cookie` on port 10445 -- so "the two
variants are browser-observably identical" is an executable assertion rather than a claim.
against *both* session modes, which turns "the two variants are browser-observably identical" into an
executable assertion rather than a claim -- see
link:doc/playwright-suite.adoc#_why_the_demo_exists[Why the Demo Exists].

This module ships no Java and produces no artifact. Nothing here reaches the production image or the
native executable.
Expand All @@ -25,84 +26,63 @@ toolchain into `target/` and installs the Chromium build Playwright drives.
== Running It

One command does everything -- toolchain install, lint, browser install, three-container bring-up,
both Playwright projects, teardown:
both Playwright projects, and teardown on a successful run:

[source,bash]
----
python3 .plan/execute-script.py plan-marshall:build-maven:maven \
run --command-args "verify -Pe2e-demo -pl demo-client"
./mvnw verify -Pe2e-demo -pl demo-client
----

When iterating on specs, hold the stack up and re-run only the tests:
A *failing* suite deliberately leaves the containers up: the npm goal aborts the build before Maven
reaches its teardown phase, so the stack you need for diagnosis is still there. Stop it with
`demo-client/scripts/stop-dev-environment.sh`, or with a `clean` build under the same profile
(`./mvnw clean -Pe2e-demo -pl demo-client`) -- the `pre-clean` teardown lives inside `e2e-demo`, so a
plain `clean` does not run it. See
link:doc/playwright-suite.adoc#_running_the_suite_locally[Running the Suite Locally].

When iterating on specs, hold the stack up and re-run only the tests. `playwright.config.js` takes
its three addresses from the environment and fails loudly when any is unset; the `e2e-demo` profile
supplies them from `demo-client/pom.xml`, so a standalone run has to export them itself:

[source,bash]
----
demo-client/scripts/start-dev-environment.sh

# Mirrors the demo.baseUrl.server, demo.baseUrl.cookie and demo.keycloak.url properties in
# demo-client/pom.xml, which remain the single source of these addresses.
export PLAYWRIGHT_BASE_URL_SERVER=https://localhost:10443
export PLAYWRIGHT_BASE_URL_COOKIE=https://localhost:10445
export KEYCLOAK_HOST_URL=https://localhost:1443

cd demo-client && npm run test
demo-client/scripts/stop-dev-environment.sh
----

`npm run test`, never `npx`: `npm run` resolves the binary from `node_modules/.bin` only, so a
missing binary is a hard failure rather than a silent registry download. The build makes the same
choice, for the same reason.

Without `-Pe2e-demo` this module is a no-op: a default reactor build downloads no Node, runs no npm
command, starts no container and produces no artifact.
`npm run test`, never `npx` -- and no Maven npm execution runs outside `-Pe2e-demo`: a default
reactor build downloads no Node, runs no npm command, starts no container and produces no artifact.
The direct `npm run test` above is the one npm invocation outside the profile, and it is a deliberate
developer opt-in rather than part of any build. Both are load-bearing choices rather than style, and
the reasoning is in link:doc/playwright-suite.adoc#_the_opt_in_mechanism[The Opt-In Mechanism].

Once the stack is up, the application is at `https://localhost:10443/assets/demo/index.html`
(server-session mode) and `https://localhost:10445/assets/demo/index.html` (cookie mode). Sign in as
`integration-user` / `integration-password`. Note the explicit filename -- the gateway serves no
directory index, so `/assets/demo/` returns `404`.

== Where the Output Lands

Everything generated sits under `target/` and is git-ignored:

[cols="1,2"]
|===
| Path | Contents

| `target/test-results/`
| Playwright JSON and JUnit results. There is no HTML reporter, so no report server is spawned.

| `target/screenshots/{project}/`
| The documentation screenshots -- `anonymous`, `authenticated-default-view`,
`full-allowlisted-view`, `claim-denied`, `logged-out` -- one parallel set per session mode.

| `target/node/`, `target/node_modules/`
| The pinned Node toolchain and the installed packages.
|===

== Layout

[cols="1,2"]
|===
| Path | What it is

| `src/main/resources/spa/`
| The SPA: `index.html`, `app.js`, `app.css`, `landing.html`. No framework, no bundler, no build
step -- the served files are the authored files, bind-mounted into the gateway containers.

| `tests/`, `fixtures/`, `utils/`
| The Playwright suite, its shared fixtures, and the Keycloak login helper.

| `playwright.config.js`
| Two projects, `session-server` and `session-cookie`, differing only in `baseURL`.
`integration-user` / `integration-password`. Note the explicit filename -- the gateway serves
link:doc/integration-sample.adoc#_no_directory_index[no directory index].

| `scripts/`
| The trimmed bring-up and teardown of exactly three containers.
|===
Everything generated sits under `target/` and is git-ignored.

== Further Reading

Both layers are authoritative; this file is only the front door.

* link:../doc/user/demo-client.adoc[Demo Client -- the BFF Integration Sample] -- the *integrator*
* link:doc/integration-sample.adoc[Demo Client -- the BFF Integration Sample] -- the *integrator*
layer: the browser-facing contract, the four identity-disclosure states, the same-origin
`returnUrl` rule, and the `gateway.yaml` a deployment needs to serve its own bundle this way.
* link:../doc/development/demo-client.adoc[Demo Client and the Playwright End-to-End Suite] -- the
*contributor* layer: the module layout, the opt-in build mechanism, the Chromium host-resolver
mapping and why it is required, and the standing prohibitions this module carries.
* link:doc/playwright-suite.adoc[Demo Client and the Playwright End-to-End Suite] -- the
*contributor* layer: the link:doc/playwright-suite.adoc#_module_layout[module layout], where a run
link:doc/playwright-suite.adoc#_running_the_suite_locally[leaves its results and screenshots], the
opt-in build mechanism, the Chromium host-resolver mapping and why it is required, and the standing
prohibitions this module carries.

Read the contributor document before changing the suite. Several of its constraints look arbitrary
and are not.
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ four identity-disclosure states an application can reach, the redirect rules the
your behalf, and the `gateway.yaml` a deployment needs so the gateway serves the application itself.

The runnable sample lives in `demo-client/` (see
link:../../demo-client/README.adoc[`demo-client/README.adoc`] for the quickstart). How that module is
link:../README.adoc[`demo-client/README.adoc`] for the quickstart). How that module is
built, and why its test suite is shaped as it is, is the contributor concern documented in
link:../development/demo-client.adoc[Demo Client and the Playwright End-to-End Suite].
link:playwright-suite.adoc[Demo Client and the Playwright End-to-End Suite].

The field-level contract for every key named here is
link:../configuration.adoc[Configuration Reference]; the setup guides for the two session variants are
link:bff-session.adoc[Server-Session BFF -- Operator Setup] and
link:bff-cookie.adoc[Cookie-Based BFF -- Operator Setup]. This document is the *sample*, not a second
link:../../doc/configuration.adoc[Configuration Reference]; the setup guides for the two session variants are
link:../../doc/user/bff-session.adoc[Server-Session BFF -- Operator Setup] and
link:../../doc/user/bff-cookie.adoc[Cookie-Based BFF -- Operator Setup]. This document is the *sample*, not a second
copy of either.

== The Browser-Facing Contract
Expand Down Expand Up @@ -80,6 +80,7 @@ what makes the whole contract reachable with a plain same-origin `fetch`.
Two properties of this table are worth stating explicitly, because assuming otherwise is the most
common way to mis-integrate:

[#_the_401_versus_302_split]
* *The `401`-versus-`302` split is PATH-based, not `Accept`-based.* Both reserved endpoints are
blind to the request's `Accept` header. `/auth/userinfo` is an XHR probe and answers
`401 application/problem+json` without a session -- it never redirects, whatever you send. Do not
Expand All @@ -104,6 +105,13 @@ The whole leg is navigations the browser performs; your application starts it an
Tokens never reach the browser at any point: the gateway holds them, and the application's only
credential is the `HttpOnly` session cookie it cannot read.

image::../../doc/resources/diagrams/demo-client-login-flow.svg[Demo-client login flow as the single-page application sees it, align=center]

The diagram draws only the legs your application participates in. The identity-provider interior --
the authorization request, the user authenticating, and the gateway's back-channel code exchange --
is deliberately not redrawn here; it is the same handshake both BFF variants perform, drawn once in
link:../../doc/resources/diagrams/bff-login-sequence.svg[the BFF login handshake diagram].

[NOTE]
.What you must configure at the identity provider
====
Expand Down Expand Up @@ -135,7 +143,7 @@ What this means for you as an integrator: *treat access logs and `Referer`-beari
callback path as sensitive*, and do not add third-party analytics or error reporting that captures
full URLs on `/auth/callback`. Nothing else in your application is affected -- the code never
reaches your code. The full statement is in
link:../architecture.adoc#_oidc_callback_response_mode[Architecture -- OIDC callback response mode].
link:../../doc/architecture.adoc#_oidc_callback_response_mode[Architecture -- OIDC callback response mode].
====

== The Four Identity-Disclosure States
Expand Down Expand Up @@ -171,6 +179,8 @@ Against the sample's `allowed_claims: ["sub", "preferred_username", "email", "gr
because the operator did not allow it.
|===

image::../../doc/resources/diagrams/demo-client-userinfo-probe.svg[Demo-client userinfo probe and the four identity-disclosure states, align=center]

The last row is the one to internalise. *The operator -- never the browser -- widens disclosure.* A
claim outside `allowed_claims` can never be disclosed even when the identity provider issued it, and
naming one explicitly is rejected `403` before any disclosure happens. An empty allowlist discloses
Expand Down Expand Up @@ -219,11 +229,12 @@ with a live session redirects straight to the validated return URL. No new login
started and no binding cookie is set. An application may therefore link to `/auth/login`
unconditionally without worrying about spurious re-authentication.

[#_serving_your_application_from_the_gateway]
== Serving Your Application from the Gateway

The sample is served through the asset terminal action
(link:../adr/0014-asset-serving-terminal-action.adoc[ADR-0014]) under a `type: asset` /
`access: public` anchor (link:../adr/0013-anchor-type-access-axes.adoc[ADR-0013]). The route is one
(link:../../doc/adr/0014-asset-serving-terminal-action.adoc[ADR-0014]) under a `type: asset` /
`access: public` anchor (link:../../doc/adr/0013-anchor-type-access-axes.adoc[ADR-0013]). The route is one
block:

[source,yaml]
Expand All @@ -249,7 +260,7 @@ Three things a deployment needs, and one it does not:
* You do *not* need a new anchor per bundle. Reusing an existing `access: public` asset anchor keeps
the anchor set -- which must stay pairwise prefix-disjoint -- unchanged.

[IMPORTANT]
[IMPORTANT#_no_directory_index]
====
*There is no directory index.* A request for `/assets/demo/` without a filename returns `404`. Every
entry point must name a concrete file: `/assets/demo/index.html`, not `/assets/demo/`. This applies
Expand Down Expand Up @@ -327,5 +338,9 @@ The sample runs unchanged against both session variants -- `session.mode: server

That is the intended property. An application is written against the reserved-path contract, and the
operator chooses the session variant on operational grounds
(link:bff-session.adoc[server-session setup], link:bff-cookie.adoc[cookie setup]) without the
(link:../../doc/user/bff-session.adoc[server-session setup], link:../../doc/user/bff-cookie.adoc[cookie setup]) without the
application knowing which was chosen.

You are not asked to take that on trust: the Playwright suite runs the same specs against both
variants, which is what makes "browser-observably identical" an executable assertion rather than a
claim. link:playwright-suite.adoc#_why_the_demo_exists[Why the Demo Exists] records the mechanism.
Loading
Loading