Injectable UX

Badge, gating, injection bundle, 403 banner and CSP.

This example runs with the dev provider and demonstrates every injection surface in one route.

Route

{
  "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" },
  "pwa": {
    "enabled": true,
    "networkOnly": {
      "excludePatterns": [
        "^https://idp\\.example\\.com/.*/\\.well-known/openid-configuration",
        "^https://idp\\.example\\.com/.*/protocol/openid-connect"
      ]
    }
  }
}

Bundle

{
  "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" }
  }]
}

injects[].when targets a path prefix; file is read at load, relative to the bundle. patch appends the data-gtw-* attributes client-side on every guard evaluation.

Try it

The packaged demo serves the gated dashboard and starts Gate One (no Docker):

cd gate-one-src/deploy/local/injectable-ux
BIN_DIR=~/gate-one-bin ./run.sh

The script prints the URLs and stops with ./run.sh down. See [deploy/local/injectable-ux](https://github.com/clad-one/gate-one/tree/main/deploy/local/injectable-ux) for the stack and its README.

To run it by hand instead:

python3 -m http.server 8080
gate-one -check -config routes.json
gate-one -config routes.json

Log in as alice (user) then bob (admin). The .danger-zone is only visible to admin; the .beta-feature is hidden before login; the export control opens a login tab and replays the action after login.

CSP

csp.mode: "nonce" emits Gate One’s own policy and stamps inline scripts. If the application already sends a CSP, prefer csp.mode: "merge"; if the application forbids inline scripts, use hash or merge and keep application scripts external.

PWA and identity endpoints

pwa.enabled wraps the route in a service worker that caches same-origin pages and assets. Any URL that carries identity must therefore be excluded with pwa.networkOnly.excludePatterns (JS regexes tested against the full URL). Otherwise the worker can serve a cached auth probe — a stale identity after login or logout — or replay a callback that carries a one-time code.

Always exclude:

  • the identity provider’s discovery, authorize, token, JWKS and end-session endpoints;

  • any token endpoint used by API or machine clients (OAuth/JWT), and the http-bridge login URL when the route uses that provider;

  • the Gate One callback (/auth/callback) and, with the embedded IAM, /t/<tenant>/callback, which carry code/state.

"pwa": {
  "enabled": true,
  "networkOnly": {
    "excludePatterns": [
      "^https://idp\\.example\\.com/.*/\\.well-known/openid-configuration",
      "^https://idp\\.example\\.com/.*/protocol/openid-connect",
      "^https://idp\\.example\\.com/.*/end_session_endpoint",
      "^http://login\\.demo\\.localhost:3000/auth/callback",
      "^http://login\\.demo\\.localhost:3000/t/[^/?#]+/callback"
    ]
  }
}

Replace idp.example.com with the issuer host. See the routes.pwa.networkOnly reference.

Note
  • UI gating is a UX layer, not authorization. Keep server-side auth.identity rules for real rights.

  • The guard cannot intercept a request the browser never sent.

  • Injection and CSP are per route; do not enable own: true unless the configured policy is complete for the application.

  • A PWA service worker must never cache identity endpoints; see PWA and identity endpoints.