Skip to content

feat(auth): tell a refused bearer, a refused account and an outage apart - #1922

Merged
cevheri merged 3 commits into
mainfrom
feat/bearer-client-contract
Oct 22, 2026
Merged

cevheri merged 3 commits into
mainfrom
feat/bearer-client-contract

Conversation

@cevheri

@cevheri cevheri commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #1914: the answers a native client on the bearer channel needs to tell a refused token, a refused account and an outage apart, and two gaps it hit in the connection list.

  • Bearer on public routes. GET /api/auth/me, POST /api/auth/logout and POST /api/db/health are on the public list but read the session, so a provider outage there read as 401 and an expired token on logout as {success:true}. The proxy now decides the Bearer header on those three as on every other API route (401 with the challenge, 503 IDENTITY_PROVIDER_UNAVAILABLE). The sign-in routes and the liveness GET still never read it.
  • Refused account. A token that verifies but maps to no role, or carries no usable username, answers 403 BEARER_NO_ROLE / BEARER_NO_IDENTITY instead of 401, so a client stops refreshing.
  • Credentials. GET /api/connections/managed strips secret fields from an editable seed for a bearer session; the web cookie session keeps today's behaviour.
  • Grant level. Governed records carry access: "read" | "write".
  • Codes. ADMIN_REQUIRED on every admin-only 403 through one helper, TRANSACTION_NOT_OWNED added to ApiErrorCode, and /api/admin/audit answers 401 AUTH_REQUIRED without a session instead of 403.

Not changed: getSession() keeps its no-throw contract, so routes behind the proxy are untouched. A native POST without Origin must send Content-Type: application/json to pass the cross-site check; now documented.

Also fixed here: the NETCONF live check's Node bundle now builds with --outdir, as the OPC UA step does. Where the runner compiles ssh2's optional sshcrypto.node, bun emits it as an asset and --outfile failed; Functional Smoke has been red on main since 3e92cc6 for this.

Tests: proxy, admin, managed list and error-code tests extended first; full suite with 100% line coverage, build, build:lib, attw and the drift guards pass locally.

The proxy now decides the Bearer header on the public routes that read a
session (GET /api/auth/me, POST /api/auth/logout, POST /api/db/health), so
an unreachable identity provider answers 503 there and an expired token on
logout answers 401 with the challenge. A verified token whose account has no
role or no usable username answers 403 BEARER_NO_ROLE or BEARER_NO_IDENTITY,
so a client stops refreshing.

GET /api/connections/managed strips every secret field from an editable seed
for a bearer session and exposes a governed record's grant level as access.
Admin-only 403s carry ADMIN_REQUIRED through one helper, TRANSACTION_NOT_OWNED
joins ApiErrorCode, and /api/admin/audit answers 401 without a session.
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…does

Where the runner compiled ssh2's optional sshcrypto.node, bun emits it as an
asset beside the bundle and --outfile exits 1 with 'cannot write multiple
output files without an output directory'. Red on main since 3e92cc6.
@sonarqubecloud

Copy link
Copy Markdown

@cevheri
cevheri merged commit c9989a6 into main Oct 22, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant