Skip to content

fix(ui): package the OIDC silent-callback page so it actually works - #2549

Open
thjaeckle wants to merge 2 commits into
eclipse-ditto:masterfrom
beyonnex-io:fix/ui-package-silent-callback
Open

thjaeckle wants to merge 2 commits into
eclipse-ditto:masterfrom
beyonnex-io:fix/ui-package-silent-callback

Conversation

@thjaeckle

Copy link
Copy Markdown
Member

Problem

ui/silent-callback.html has never worked in any environment. It was added by c2eda4f "support silent token refresh" alongside the OIDC wiring in authorization.ts, but the packaging half was never done — and the page carried two further defects that only a real browser would surface.

1. It never reaches the image. ui/Dockerfile copies only ./index.html and ./dist; the page sits at ui/ root, so it is copied by neither. Confirmed inside a running eclipse/ditto-ui container rather than inferred: the web root holds index.html, 50x.html and dist/{main.css,main.js}, and a filesystem-wide search for *callback* returns nothing.

2. build.mjs could not have supplied it either. It has a single entry point (main.ts), and its .html loader is text — which inlines HTML imported from TypeScript as a string rather than emitting standalone HTML files. Nothing imports the page, so COPY ./dist had nothing to pick up.

3. The page could not run even if served. It did:

import('oidc-client-ts').then(({ UserManager }) => {
    const userManager = new UserManager();

'oidc-client-ts' is a bare ESM specifier — unresolvable without an import map, and the page has none. And new UserManager() with no argument throws: UserManagerSettingsStore dereferences args.redirect_uri before any defaulting.

Why it went unnoticed. The only reference to the page anywhere is the environmentTemplates.json default, http://localhost:8000/silent-callback.html — the esbuild dev server, whose servedir: '.' made the file reachable during local development and masked all three problems. Beyond that, UserManager.signinSilent() returns from its refresh-token branch whenever user?.refresh_token is set:

if (user?.refresh_token) {
  return await this._useRefreshToken({ ... });
}

so any client requesting offline_access never loads this page at all.

Fix

  • ui/silent-callback.ts (new) — the script becomes a real entry point, so esbuild resolves and bundles oidc-client-ts instead of leaving a bare specifier.
  • ui/build.mjsentryPoints: ['main.ts', 'silent-callback.ts'].
  • ui/silent-callback.html — loads ./dist/silent-callback.js, mirroring how index.html loads ./dist/main.js.
  • ui/DockerfileCOPY ./silent-callback.html .

The placeholder settings in silent-callback.ts are deliberate and commented: signinSilentCallback() delegates to IFrameNavigator.callback(), which reads nothing from the settings but the optional iframeNotifyParentOrigin (defaulting to the page's own origin). authority and client_id are required by UserManagerSettings but never touched on this path — and passing an object at all is what avoids the constructor throw.

Also included: ui/.dockerignore

With no .dockerignore, the whole ui/ tree went to the daemon as build context — node_modules included — so a three-COPY build took minutes. An allowlist was chosen over a denylist so that adding a COPY without extending the list fails the build rather than silently depending on whatever is lying around in the working tree.

Build time drops from minutes to ~10s, and the image contains a byte-for-byte identical set of files.

Verification

  • npm run build (tsc -noEmit && node build.mjs) passes, emitting dist/silent-callback.js (65 kB).
  • The bundle carries the real contract — the "oidc-client" message source, postMessage and keepOpen are all present — with 0 dynamic imports and 0 unresolved bare specifiers.
  • Building ui/Dockerfile and serving the image:
Path Status Content-Type Bytes
/silent-callback.html 200 text/html 172
/dist/silent-callback.js 200 application/javascript 65705
/index.html 200 text/html 7401
/dist/main.js 200 application/javascript 945817
  • Image contents identical before and after adding .dockerignore: 50x.html, index.html, silent-callback.html, dist/{main.css, main.css.map, main.js, main.js.map, silent-callback.js} at the same sizes.

Not verified

The renewal round trip itself needs a browser session with an IdP that issues no refresh token, waiting for an access token to approach expiry — I could not exercise that. What is verified is that the page and its bundle are served correctly and that the bundled code contains the message contract AbstractChildWindow validates.

thjaeckle and others added 2 commits September 9, 2026 16:58
silent-callback.html has never worked in any environment. It was added by
c2eda4f "support silent token refresh" together with the OIDC wiring in
authorization.ts, but the packaging half was never done, and the page had two
further defects that only a real browser would surface:

1. It never reaches the image. ui/Dockerfile copies only ./index.html and
   ./dist; the page sits at ui/ root, so it is copied by neither. Confirmed in
   a running eclipse/ditto-ui container - the web root holds index.html,
   50x.html and dist/{main.css,main.js}, and a filesystem-wide search for
   *callback* returns nothing.

2. build.mjs could not have supplied it either. It has a single entry point
   (main.ts), and its `.html` loader is `text`, which inlines HTML imported
   from TypeScript rather than emitting standalone HTML. Nothing imported the
   page, so `COPY ./dist` had nothing to pick up.

3. The page could not run even if served. It did `import('oidc-client-ts')` - a
   bare ESM specifier, unresolvable without an import map, and the page had
   none. It also called `new UserManager()` with no argument, which throws:
   UserManagerSettingsStore dereferences `args.redirect_uri` before any
   defaulting.

The only reference to the page anywhere is the environmentTemplates.json
default, http://localhost:8000/silent-callback.html - the esbuild dev server,
whose `servedir: '.'` made the file reachable during local development and
masked all of the above.

Fix all three: move the script into silent-callback.ts and add it to
entryPoints so esbuild resolves and bundles oidc-client-ts, point the page at
./dist/silent-callback.js the way index.html points at ./dist/main.js, and copy
the page in the Dockerfile.

The placeholder settings in silent-callback.ts are deliberate.
signinSilentCallback() delegates to IFrameNavigator.callback(), which reads
nothing from the settings but the optional iframeNotifyParentOrigin; authority
and client_id are required by UserManagerSettings yet never touched on this
path. Passing an object at all is what avoids the constructor throw.

Verified end to end:

  * `npm run build` (tsc -noEmit && node build.mjs) passes and emits
    dist/silent-callback.js (65 kB).
  * The bundle carries the real contract - the "oidc-client" message source,
    postMessage and keepOpen are all present - with 0 dynamic imports and 0
    unresolved bare specifiers left.
  * Building ui/Dockerfile and serving the image returns 200 for both
    /silent-callback.html (text/html) and /dist/silent-callback.js
    (application/javascript), alongside the existing index.html and main.js.

Note the renewal path itself is only exercised when no refresh token is
present: UserManager.signinSilent() returns from its refresh-token branch
whenever `user?.refresh_token` is set, so a client requesting offline_access
never loads this page. That is why the breakage went unnoticed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tree

The image serves three things - index.html, silent-callback.html and dist/ -
but with no .dockerignore the entire ui/ tree was sent to the daemon as build
context, node_modules included. A three-COPY build took minutes.

Allowlist rather than denylist, so adding a COPY without extending this list
fails the build instead of silently depending on whatever happens to be lying
around in the working tree.

Build time drops from minutes to ~10s, and the image is byte-for-byte the same
set of files: 50x.html, index.html, silent-callback.html and dist/{main.css,
main.css.map, main.js, main.js.map, silent-callback.js} at identical sizes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thjaeckle thjaeckle self-assigned this Sep 9, 2026
@thjaeckle thjaeckle added the bug label Sep 9, 2026
@thjaeckle thjaeckle added this to the 3.9.8 milestone Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant