Injectable UX

A user badge, a login guard, gated controls, a 403 banner and a PWA shell without changing the application.

Useful if you want to…

  • show who is logged in and a logout button on an application you cannot modify;

  • turn a plain 401 into a login tab that replays the request after login, without a page reload;

  • hide or gate controls by role, scope or claim client-side, even when the markup is fixed;

  • add a banner, a stylesheet, a script or a PWA manifest per route;

  • show a readable page or a banner when the gateway itself refuses a request on identity (403), while keeping the JSON 403 for fetch().

The application source does not change. Gate One injects a small guard script and, optionally, an operator-provided bundle.

What can be injected

FeatureWhat it does

User badge

Collapsed username on the top edge; expands to show chosen claims/roles/scopes and a logout button. Configured per route with userBadge.

Guard

Intercepts the gateway’s 401, opens the login surface in a tab, replays the failed request after login and evaluates UI gating. Active on frontend routes; opt out with authGuard.enabled: false.

UI gating

data-gtw-* attributes in the markup, or uiGate CSS selector rules when the markup cannot change. Roles, scopes, claims, before/after activation and hiding.

Inject bundle

HTML/JS/CSS insertions at a document position (head or body-end), page prefix and order — a script can then place content at a specific element — plus client-side data-* patches. Referenced by route.inject.

PWA

Manifest, icons, service worker and API cache policy per route. Identity endpoints must be excluded from the cache (pwa.networkOnly).

Forbidden UX

authGuard.forbidden adds a dismissable banner on the gateway’s own identity 403, or redirects browser navigations to a page.

CSP

csp.mode off, merge, hash or nonce; the guard is class-only and its runtime style carries the nonce.

Example configuration

{
  "host": "app.demo.localhost",
  "path": "/*",
  "frontend": true,
  "backend": "http://127.0.0.1:8080",
  "auth": {},
  "inject": "bundle.json",
  "userBadge": {
    "position": "right",
    "claims": ["preferred_username", "email"],
    "roles": ["admin"],
    "logoutLabel": "Sign out"
  },
  "uiGate": [
    { "selector": ".danger-zone", "roles": ["admin"] },
    { "selector": ".beta-feature", "hidden": "before" },
    { "selector": "[data-feature=export]", "auth": "before" }
  ],
  "authGuard": { "forbidden": { "banner": true } },
  "csp": { "mode": "nonce" }
}

The bundle referenced by inject:

{
  "injects": [
    {
      "name": "announcement",
      "target": "body-end",
      "when": "/dashboard",
      "file": "banner.html"
    },
    {
      "name": "brand-style",
      "target": "head",
      "style": ".demo-brand{letter-spacing:.02em}"
    }
  ],
  "patch": [
    {
      "selector": "[data-feature=beta]",
      "attrs": { "data-gtw-auth-hidden": "before" }
    }
  ]
}

Complete files: routes.json, bundle.json.

Placing content at a specific element

injects[].target is a document position, not a selector. It accepts only head (default) or body-end, plus order for the position within that target and when for a path prefix. There is no "insert next to `.article`" field.

Two mechanisms cover placement, and they are different:

MechanismWhat it can do

patch

Sets attributes on the elements a CSS selector matches (data-gtw-*), client-side, on every guard evaluation cycle. It never inserts, moves or removes a node.

script / file

The injected JavaScript runs in the page context, so it can query the DOM and place content wherever it belongs (insertAdjacentHTML, insertBefore, append, …).

To anchor generated markup to an existing element, inject a small script at body-end and let it do the placement:

{
  "name": "article-note",
  "target": "body-end",
  "when": "/wiki/",
  "script": "var a=document.querySelector('#mw-content-text'); if (a) a.insertAdjacentHTML('afterbegin', '<p class=article-note>Reviewed by the platform.</p>');"
}

The same pattern with a larger script is the proxied-site example: the bundle injects the Wikipedia+ userscript at head, and the script builds its panel and positions the arrow relative to that panel. target only decides where the <script> tag lands in the document.

Scripts execute once when the injected markup is parsed. If the page rerenders its DOM, the script is not replayed; patch is the mechanism that the guard re-applies (see the limits below).

PWA and identity endpoints

pwa.enabled adds a service worker that caches pages and assets. Any URL that carries identity must be excluded with pwa.networkOnly.excludePatterns (JS regexes tested against the full URL): the identity provider’s discovery, authorize, token, JWKS and end-session endpoints; any OAuth/JWT token endpoint used by API or machine clients; the http-bridge login URL; and the Gate One callback (/auth/callback) or the embedded IAM’s /t/<tenant>/callback.

A cached auth probe replays a stale identity after login or logout; a cached callback replays a one-time code. The Injectable UX example shows the patterns, and routes[.pwa.networkOnly] is the field reference.

Markup or config?

If you control the HTML, prefer the data-gtw- attributes; they are explicit and survive selector changes. If you cannot modify the application, use uiGate rules by selector. Markup wins: an element carrying a data-gtw- attribute ignores the equivalent uiGate rule.

Activation is gated; deactivation is always free. Unchecking a box, closing a details, resetting a form or returning a value control to its OFF value never opens a login. This avoids trapping anonymous users in an action they cannot complete.

Own account UI

If the application owns its navigation, omit userBadge and render the session in the application’s own burger menu or side panel. Gate One still injects the guard and exposes the session probe; the account markup stays in the application. See Bring your own account panel and the account panel example.

External sites

A route with upstream can proxy an external site and inject a userscript into every HTML page. The browser installs no extension and the upstream site contains no Gate One code. See Enhance a proxied site.

CSP is the real constraint

The guard uses classes and a nonced runtime <style>; it does not need style="…" attributes. If the application already sends a CSP:

  • csp.mode: "merge" keeps the origin policy and appends the guard hashes;

  • csp.mode: "nonce" emits Gate One’s policy and stamps inline scripts;

  • csp.mode: "hash" avoids rewriting the body but requires external application scripts;

  • own: true removes the origin CSP only in nonce/hash mode and is opt-in and logged.

Injected content must satisfy the policy. A nonce never applies to a style attribute.

The full CSP guide — the four modes, styles, rollout and violation reading — is Get a strict Content Security Policy. The data-gtw-* attribute contract behind uiGate is Declarative UI gating.

Important
  • Injection is a response transform, not a security boundary. Use auth.identity for route-level rights; the backend must still enforce resource-level authorization.

  • The guard cannot recover a request that the browser never sent (for example a navigation blocked before the request).

  • A page that replaces its DOM can remove injected elements; the guard re-applies patches on its evaluation cycle, but a full SPA rerender may need an application hook.

  • UI gating hides controls; it does not grant or revoke server-side rights. Combine it with auth.identity.

Configuration reference

See also

  • Guides — task-oriented deep dives: strict Content Security Policy and declarative UI gating.