Problem/Motivation
On a site that also runs simple_oauth, the assurance token path cannot work at all, and the failure is silent from FileGate's perspective: the bridge request never reaches the controller and nothing is logged by the module.
POST to /api/file-gate/assurance/bridge?<signed grant> with a valid IdP-issued Authorization: Bearer <JWT> returns 401 with:
www-authenticate: Bearer realm="OAuth", error="access_denied", error_description="Access token could not be verified"
That response comes from simple_oauth's authentication provider, not from BridgeController. simple_oauth claims any request carrying a Bearer token, tries to verify it as a local OAuth token, fails (it is an external IdP token), and 401s during authentication — before routing invokes the controller designed to validate exactly that header. The user completes hardware step-up and redemption still fails with a misleading OAuth error.
Steps to reproduce
- Enable simple_oauth alongside file_gate_assurance.
- Mint a grant for an assurance-gated field and build the bridge URL.
- POST to the bridge with no Authorization header: the controller answers with its own JSON challenge (
insufficient_user_authenticationplusacr_values). Working as designed. - POST again with any well-formed JWT Bearer (even one with a garbage signature): simple_oauth's
www-authenticate401 and a Drupal HTML error page come back, and no FileGate watchdog entry is written.
Proposed resolution
Pin the route's authentication providers: add options: _auth: ['cookie'] to file_gate_assurance.bridge so provider-based authentication ignores the foreign Bearer header and the controller receives it intact. The same-origin SSO session path (cookie) is unaffected. Audit the other open-by-design POST routes (webauthn/assert/options, webauthn/assert) for the same exposure — their clients do not send Authorization today, but the pin is cheap defence.
Remaining tasks
Patch, kernel test covering a foreign-Bearer POST reaching the controller with simple_oauth enabled, re-run of the manual E2E checklist §1.
User interface changes
None.
API changes
None.
Data model changes
None.
Found on a staging E2E run, 2026-08-02. GitHub mirror issue: Wilkes-Liberty/file_gate#54
Comments
Comment #3
jmcerdaFixed in 611f0f6 on 1.x (merge GitHub PR #55, all CI green including the new
RouteAuthPinTest).The three open-by-design token endpoints (
file_gate_assurance.bridge,webauthn/assert/options,webauthn/assert) now pinoptions: _auth: ['cookie'], so a global authentication provider that consumesAuthorization(simple_oauth being the common case) can no longer reject the external IdP Bearer/DPoP token before the controller validates it. A kernel test guards the pin against regression.Ships in the next tagged release; the manual E2E checklist §1 re-run happens against that release.
Comment #5
jmcerdaReopening: the 1.5.1 fix does not hold on a site where the intercepting provider is global, which is the configuration that motivated this issue.
The _auth: ['cookie'] route pin is consulted too late to help. Core authenticates before routing: AuthenticationSubscriber::onKernelRequestAuthenticate runs at KernelEvents::REQUEST priority 300, RouterListener at 32, and the _auth provider filter (onKernelRequestFilterProvider) at 31. simple_oauth registers its provider with global: TRUE, so its applies() fires on Authorization-header presence alone and authenticate() runs at priority 300 — before the route (and its _auth option) exists. A token it cannot validate throws right there, and the request never reaches the bridge controller.
Verified empirically on 1.5.1 (enabled, caches rebuilt):
- POST /api/file-gate/assurance/bridge with no Authorization header → {"error":"Missing file parameter."} HTTP 400 — the controller answers.
- The same POST with any foreign Bearer → HTTP 401 before the controller.
- GET / (front page) with the same Bearer → HTTP 401. Route-independent, so no route option can ever fix it.
RouteAuthPinTest only asserts the route option is present, so it passes with the defect live — it cannot fail in the real failure mode.
Directions that actually work, in order of preference:
1. An http middleware in file_gate that, for file_gate's own paths, moves the Authorization header into a request attribute before the kernel's request events run, so no authentication provider ever sees it; the bridge/redeem controllers read the attribute. Middleware wraps the kernel, so this holds regardless of which global providers a site runs.
2. Upstream simple_oauth: abstain (rather than throw) on tokens whose iss is not its own. Correct long-term, but file_gate cannot depend on every consumer site carrying it.
3. Moving the token out of the Authorization header for the bridge POST — works, but breaks standard OAuth/DPoP wire semantics.
Full write-up with reproduction: https://github.com/Wilkes-Liberty/file_gate/issues/56
Comment #6
jmcerdaReopening: the 1.5.1 fix does not work. Verified on a live site immediately after deploying 1.5.1 (version confirmed, caches rebuilt): the garbage-Bearer probe still returns simple_oauth's
www-authenticate: Bearer realm="OAuth"401, not the controller JSON.Why
_authcannot fix this. simple_oauth registers its authentication providerglobal: TRUE, and Drupal authenticates global providers in the request phase before routing. The_authroute option is only consulted after routing — it can reject the provider that was used, but it cannot stop a global provider from 401-ing an invalid token pre-routing. The route option is inert for this failure mode, and the 1.5.1 kernel test asserted the option's presence (structural), not the behavior, which is how the gap survived CI.Correct fix (v2). Transport the token in a module-specific header the interceptor never matches —
X-File-Gate-Token: <JWT>— consumed by the bridge as equivalent toAuthorization: Bearer, withAuthorizationretained as the spec-clean path for stacks without global Bearer consumers. The step-up page JS switches to the custom header; DPoP proof continues in theDPoPheader. The regression test becomes behavioral: a request carrying the custom header must reach token validation, not the authentication challenge.Patch to follow on 1.x; will ship in 1.5.2.
Comment #7
jmcerdaFixed for real in 5a0ba08 (merge GitHub PR #57), released in 1.5.2.
An
AuthorizationShieldhttp middleware stashes the Bearer/DPoPAuthorizationvalue into a request attribute and removes the header on File Gate's own token endpoints (/api/file-gate/download,/api/file-gate/assurance/bridge) before any authentication provider runs; handlers read the stash transparently. Clients keep standardAuthorizationwire semantics — no client changes. Regression coverage is behavioral this time: shield unit tests in both directions plus a stashed-attribute redeem through the bridge and the direct download.Sites running simple_oauth should update to 1.5.2 — the assurance token redeem path does not work on 1.5.0/1.5.1 beside a global authentication provider.