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

  1. Enable simple_oauth alongside file_gate_assurance.
  2. Mint a grant for an assurance-gated field and build the bridge URL.
  3. POST to the bridge with no Authorization header: the controller answers with its own JSON challenge (insufficient_user_authentication plus acr_values). Working as designed.
  4. POST again with any well-formed JWT Bearer (even one with a garbage signature): simple_oauth's www-authenticate 401 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

jmcerda created an issue. See original summary.

  • jmcerda committed d09861d2 on 1.x
    Record lab notes: OIDC redeem run blocked by Bearer interception (#...
jmcerda’s picture

Status: Active » Fixed

Fixed 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 pin options: _auth: ['cookie'], so a global authentication provider that consumes Authorization (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.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

jmcerda’s picture

Status: Fixed » Active

Reopening: 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

jmcerda’s picture

Reopening: 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 _auth cannot fix this. simple_oauth registers its authentication provider global: TRUE, and Drupal authenticates global providers in the request phase before routing. The _auth route 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 to Authorization: Bearer, with Authorization retained 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 the DPoP header. 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.

jmcerda’s picture

Status: Active » Fixed

Fixed for real in 5a0ba08 (merge GitHub PR #57), released in 1.5.2.

An AuthorizationShield http middleware stashes the Bearer/DPoP Authorization value 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 standard Authorization wire 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.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.