Skip to content

fix(auth): JwtAuthFilter eager-401 with @PermitAll opt-out#4903

Open
Yicong-Huang wants to merge 4 commits intoapache:mainfrom
Yicong-Huang:refactor/jwt-filter-eager-401
Open

fix(auth): JwtAuthFilter eager-401 with @PermitAll opt-out#4903
Yicong-Huang wants to merge 4 commits intoapache:mainfrom
Yicong-Huang:refactor/jwt-filter-eager-401

Conversation

@Yicong-Huang
Copy link
Copy Markdown
Contributor

@Yicong-Huang Yicong-Huang commented May 4, 2026

What changes were proposed in this PR?

Align the 4 microservices' JwtAuthFilter with amber's behavior: return 401 directly from the filter with an RFC 6750 WWW-Authenticate: Bearer … challenge, instead of silently passing through to Dropwizard's @Auth injection.

@PermitAll (JSR-250) opts a resource out of the no-header 401. An invalid token still fails — a tampered/stale token is never treated as anonymous. The only existing consumer is DatasetResource.getDatasetCover for anonymous public-dataset reads.

Any related issues, documentation, discussions?

Closes #4901.

Out of scope: RolesAllowedDynamicFeature is registered only in amber. The 4 microservices' @RolesAllowed annotations are currently decorative; workflow-compiling-service registers no auth filter at all. Worth a separate issue.

How was this PR tested?

Auth/test covers JwtAuthFilter (header-missing / non-Bearer / invalid-token / valid-token; method- and class-level @PermitAll; resource-info-absent fallback) and UnauthorizedExceptionMapper (status, challenge passthrough, no entity body). Format / lint clean; all 4 microservices recompile.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.7)

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file refactor Refactor the code common platform Non-amber Scala service paths labels May 4, 2026
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 4, 2026

Codecov Report

❌ Patch coverage is 25.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.55%. Comparing base (786a920) to head (655db3b).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
.../texera/service/ComputingUnitManagingService.scala 0.00% 1 Missing ⚠️
...cala/org/apache/texera/service/ConfigService.scala 0.00% 1 Missing ⚠️
.../scala/org/apache/texera/service/FileService.scala 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4903      +/-   ##
============================================
+ Coverage     42.41%   42.55%   +0.13%     
- Complexity     2155     2187      +32     
============================================
  Files           954      954              
  Lines         34675    34686      +11     
  Branches       3629     3633       +4     
============================================
+ Hits          14709    14760      +51     
+ Misses        19061    19015      -46     
- Partials        905      911       +6     
Flag Coverage Δ
access-control-service 39.88% <100.00%> (+0.34%) ⬆️
amber 43.17% <ø> (+0.30%) ⬆️
computing-unit-managing-service 0.00% <0.00%> (ø)
config-service 0.00% <0.00%> (ø)
file-service 33.16% <0.00%> (-0.09%) ⬇️
workflow-compiling-service 47.72% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Today JwtAuthFilter silently passes through any request that lacks a
valid Bearer token; the 401 only surfaces later when Dropwizard's
@Auth injection fails. amber's toastshaman path returns 401 directly
from its filter with a WWW-Authenticate challenge — strictly more
correct.

Align the microservice filter:

- No `Authorization: Bearer …` header → throw 401 with bare
  `WWW-Authenticate: Bearer realm="texera"` (RFC 6750 §3 challenge).
- Header present but token verification / claim extraction fails →
  throw 401 with `error="invalid_token"` so a well-behaved client
  can discard the bad token instead of retrying.
- Header present and valid → install SecurityContext as before.

@permitAll opt-out: a resource method (or class) annotated with
`jakarta.annotation.security.PermitAll` skips the eager 401 only on
the "no header" path. The `@Auth Optional[SessionUser]` parameter is
then injected as empty. An invalid token still 401s on @permitAll
endpoints — a tampered or stale token is never silently treated as
anonymous.

The single in-tree consumer of the optional pattern is
`file-service/.../DatasetResource.getDatasetCover` (anonymous read of
public dataset covers); annotate it with @permitAll.

Failure is signaled by throwing WebApplicationException rather than
abortWith — the JAX-RS-idiomatic shape, plus it composes with
Dropwizard's WebApplicationExceptionCatchingFilter when reused
elsewhere.

Tests: 9-case JwtAuthFilterSpec covering required-auth (no header /
non-Bearer / unverifiable / valid), method-level @permitAll
(unauthenticated → pass / invalid token → 401 / valid → SecurityContext),
class-level @permitAll, and resourceInfo-absent fallback to
required-auth.

Common/auth gains two test-scope deps (jakarta.annotation-api for
@permitAll inspection; jersey-common to provide a RuntimeDelegate so
Response.build() works in unit tests without a Jersey runtime).

Closes apache#4901
@Yicong-Huang Yicong-Huang changed the title refactor(auth): JwtAuthFilter eager-401 with @PermitAll opt-out fix(auth): JwtAuthFilter eager-401 with @PermitAll opt-out May 4, 2026
@Yicong-Huang Yicong-Huang force-pushed the refactor/jwt-filter-eager-401 branch from ef84db6 to 7a95fbe Compare May 4, 2026 06:42
Building a JAX-RS Response from inside the filter requires a
RuntimeDelegate on the classpath. The earlier attempt added
jersey-common 3.0.12 to common/auth's test classpath to satisfy that;
amber declares `Auth % "test->test"` in build.sbt, so this jar bled
into amber's test classpath and triggered a LinkageError on
javax.ws.rs.ext.RuntimeDelegate (mismatched ClassLoader on the same
class) — breaking 4 WorkflowAccessResourceSpec tests.

Avoid the dependency entirely: throw a custom `UnauthorizedException`
(extends RuntimeException) carrying the RFC 6750 §3 challenge string
as a field. A new `UnauthorizedExceptionMapper` translates it to a
`401` with `WWW-Authenticate` at the JAX-RS edge. Tests inspect the
field directly — no Response build, no RuntimeDelegate needed, no
test-classpath cross-contamination.

Side benefits: `BearerChallenge` and `InvalidTokenChallenge` move to
the JwtAuthFilter companion as exposed `val`s, so the contract is
asserted via plain string equality.

Each of the 4 microservices that registers `JwtAuthFilter` now also
registers `UnauthorizedExceptionMapper`. amber's existing tests pass
unchanged.
scalafmt: rewrap the 4-symbol auth import line in each microservice's
Application class — the previous one-liner blew the column limit once
`UnauthorizedExceptionMapper` joined the import list.

Tests: add `UnauthorizedExceptionMapperSpec` covering the three things
that matter at the JAX-RS edge — status is 401, the exception's
challenge string is forwarded verbatim into `WWW-Authenticate`, and
the response has no entity body (so clients see the auth challenge
instead of a JSON error). Test classpath needs a Jersey
RuntimeDelegate to call `Response.build()`, so add `dropwizard-jersey`
as a Test-only dep on common/auth.
@Yicong-Huang Yicong-Huang force-pushed the refactor/jwt-filter-eager-401 branch from f2473d9 to 655db3b Compare May 6, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

common dependencies Pull requests that update a dependency file platform Non-amber Scala service paths refactor Refactor the code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JwtAuthFilter: switch to eager 401 on missing/invalid token, opt-out via @PermitAll

2 participants