From b6f7428922800021665c85aad6c90c08b337c28f Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Thu, 8 Oct 2026 20:30:13 -0400 Subject: [PATCH] Ignore bearer storage in session-authenticated admin shells --- .github/workflows/ci.yml | 7 ++++++ README.md | 3 +++ internal/ui_dist/index.html | 3 +++ test/shell-session.test.cjs | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 test/shell-session.test.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 828c30a..78adf7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,13 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Test shell session credential isolation + run: node --test test/*.test.cjs + - uses: actions/setup-go@v5 with: go-version: '1.26' diff --git a/README.md b/README.md index 7795e3b..cabc678 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,9 @@ adminui.Handler(adminui.ShellOptions{ Use `AuthModeSession` when the host authenticates admin APIs with same-origin cookies. The default bearer-token mode preserves the standalone plugin shell behavior. +Session mode ignores stored bearer tokens and does not forward them to APIs or +embedded tools, or change bearer-token storage. The host must authenticate and +authorize the shell, each contributed tool, and every API request. ## Host Conformance diff --git a/internal/ui_dist/index.html b/internal/ui_dist/index.html index 8e9f7dc..d2e768e 100644 --- a/internal/ui_dist/index.html +++ b/internal/ui_dist/index.html @@ -372,6 +372,7 @@

Management pages

let currentGrantedScopes = []; function storedToken() { + if (!requiresBearerToken) return ''; try { return window.localStorage.getItem(tokenStorageKey) || ''; } catch (_) { @@ -380,6 +381,7 @@

Management pages

} function storeToken(token) { + if (!requiresBearerToken) return; try { window.localStorage.setItem(tokenStorageKey, token); } catch (_) { @@ -388,6 +390,7 @@

Management pages

} function clearToken() { + if (!requiresBearerToken) return; try { window.localStorage.removeItem(tokenStorageKey); } catch (_) { diff --git a/test/shell-session.test.cjs b/test/shell-session.test.cjs new file mode 100644 index 0000000..a5c0adb --- /dev/null +++ b/test/shell-session.test.cjs @@ -0,0 +1,48 @@ +const assert = require('node:assert/strict') +const fs = require('node:fs') +const path = require('node:path') +const test = require('node:test') +const vm = require('node:vm') + +const html = fs.readFileSync(path.join(__dirname, '../internal/ui_dist/index.html'), 'utf8') +// Execute the shipped storage/header functions. This checks browser effects, +// rather than asserting the implementation contains a particular guard string. +const start = html.indexOf(' function storedToken() {') +const end = html.indexOf(' function scopeStrings(', start) +assert.ok(start >= 0 && end > start) +const functions = html.slice(start, end) + +function shell(mode, stored = 'fixture-stale-bearer') { + const effects = [] + const storage = new Map([['workflow.admin.token', stored]]) + const context = vm.createContext({ + window: { localStorage: { + getItem(key) { effects.push('read'); return storage.get(key) }, + setItem(key, value) { effects.push('write'); storage.set(key, value) }, + removeItem(key) { effects.push('remove'); storage.delete(key) }, + } }, + }) + vm.runInContext(`const requiresBearerToken = ${JSON.stringify(mode)} !== 'session'; const tokenStorageKey = 'workflow.admin.token';\n${functions}`, context) + return { run: code => vm.runInContext(code, context), effects, storage } +} + +test('session APIs and embedded bridges cannot receive a stale bearer token', () => { + const session = shell('session') + assert.equal(session.run('storedToken()'), '') + assert.equal(session.run('authHeaders({accept: "application/json"}).Authorization'), undefined) + assert.equal(session.run('authHeaders({accept: "application/json"}).accept'), 'application/json') + session.run('storeToken("fixture-new"); clearToken()') + assert.deepEqual(session.effects, []) + assert.equal(session.storage.get('workflow.admin.token'), 'fixture-stale-bearer') +}) + +test('bearer and unknown modes retain the protected bearer behavior', () => { + for (const mode of ['bearer', 'invalid', undefined]) { + const bearer = shell(mode) + assert.equal(bearer.run('authHeaders().Authorization'), 'Bearer fixture-stale-bearer') + bearer.run('storeToken("fixture-new")') + assert.equal(bearer.run('storedToken()'), 'fixture-new') + bearer.run('clearToken()') + assert.equal(bearer.storage.size, 0) + } +})