Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-auth): run the D5.1 /oauth2/authorize env-access gate for a signed bearer (#8102) - #8175
Conversation
…a signed bearer (#8102) ADR-0069 D5.1's cloud-as-IdP gate resolved its subject with an inline copy of resolveActor -- line for line the same logic, in a second place. #8101 fixed one bug in the shared resolver and left the copy untouched, so the two diverged. The shared resolver learned that a bearer credential must have its signature stripped before lookup: bearer() hands clients the signed form in set-auth-token (the documented API-lane credential) and accepts it back, while session.token stores the unsigned value. The copy guarding /oauth2/authorize kept looking the signed credential up verbatim and resolved nothing. The unresolved case here is deliberately fail-open, so that miss did not deny the request -- it skipped the check entirely. An authenticated caller on the documented API lane was read as unauthenticated, and against a skip_consent client was issued an authorization code the gate would have refused. Delete the copy and call resolveActor. Two resolution sites are what let them diverge, so a second corrected copy would not have fixed the class. The fail-open default for genuinely unauthenticated callers is preserved unchanged. resolveActor also returns activeOrgId; this gate deliberately does not consume it -- the D5.1 host contract is (userId, clientId) and the control plane derives org membership from the user itself. Pinned by a dogfood gate that arms a DENYING gate and drives /oauth2/authorize over the cookie lane and both accepted bearer spellings, asserting the gate was actually invoked with the caller as its subject and the request refused rather than issued a code. The cookie and raw-token lanes are controls that pass on the broken build too; the signed-bearer lane is the pin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
…th2-authorize-inline-token-lookup
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
The Lane.pinsTheDefect field was declared on every lane and read by nothing -- the same declared-but-unenforced shape this file exists to pin. Assert it: the suite now fails if a later edit makes a second lane the discriminator or drops the signed-bearer lane, either of which would leave the file green while measuring something other than #8102. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8102
ADR-0069 D5.1's cloud-as-IdP gate (
oidcAuthorizeGate) is what enforces org-membership / app-assignment before the OIDC OP issues an authorization code. Its branch resolved the acting subject with an inline copy of the sharedresolveActor— line for line the same logic, in a second place — and the two diverged the moment one of them was fixed.#8101 taught
resolveActorthat a bearer credential must have its signature stripped before lookup:bearer()hands clients the signed form in theset-auth-tokenresponse header (the documented API-lane credential) and accepts it back, whilesession.tokenstores the unsigned value. The copy guarding/oauth2/authorizekept looking the signed credential up verbatim, and so resolved nothing.Why this is
securityand not a lookup missThe unresolved case at this endpoint is deliberately fail-open — a genuinely anonymous caller must fall through so the OP can redirect them to log in. So an authenticated caller holding the signed bearer was read as unauthenticated, and the env-access check was not denied but never evaluated at all.
Measured on the unfixed build, with the gate armed to DENY and a
skip_consentclient:The gate was never invoked and an authorization code was actually issued to a subject it would have refused. The issue was written from a code reading and explicitly flagged that no live bearer-driven
/oauth2/authorizehad been measured; this closes that gap with a real request.Impact stays bounded, as the issue itself said:
oidcAuthorizeGateis set only on the cloud control plane (unset in open editions / self-host, where there is no gate at all), and the OP's authorize endpoint is normally browser/cookie-driven — the cookie branch always normalized and was never affected.The fix
Delete the inline copy; call the shared
this.resolveActor(ctx). Two resolution sites are what let them diverge, so correcting the copy in place would have left the same class open.The fail-open default for genuinely unauthenticated callers is unchanged and deliberately preserved — that was never the defect, and the pin has a case asserting it still holds.
resolveActoralso returnsactiveOrgId; this gate deliberately does not consume it (the D5.1 host contract is(userId, clientId), and the control plane derives org membership from the user itself), and the site now says so rather than dropping it silently.The pin
oidcAuthorizeGatepreviously appeared in no test file at all. The new dogfood gate drives/oauth2/authorizeacross three credential lanes against an armed, denying gate:set-auth-tokenIt closes the three ways this surface can be green while proving nothing:
applyConfigPatchand asserts it was actually called, with the right userId. A status code alone cannot separate "denied" from "never evaluated".Verification
origin/mainand rebuilding — exactly one lane went red, with the predicted diagnosis.@objectstack/dogfoodfull suite: 101 files, 680 passed / 3 skipped (re-run after mergingmain).@objectstack/plugin-auth: 45 files, 1069 passed.typecheckclean on both packages; ESLint clean.check:nul-bytes,check:changeset-gate-self-tests,check:docs-audit-scope,check:objectui-changeset,check:test-source-alias,check-changeset-no-major, plus the three off-list ratchets —check:query-options-erasure,check:type-check-debt(ledger not raised) andcheck:engine-double-contract.Generated by Claude Code