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
@Yicong-Huang
Copy link
Copy Markdown
Contributor Author

The missing three lines of coverage are on the target services who uses this auth filter. Those services need additional efforts on adding more tests for their own logic.

@Yicong-Huang
Copy link
Copy Markdown
Contributor Author

@kunwp1 could you please help review this Pr? Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the shared common/auth JWT authentication behavior used by Texera’s microservices to eagerly return RFC 6750-compliant 401 Unauthorized responses (with WWW-Authenticate: Bearer …) from the auth layer, while allowing specific endpoints to opt out of the missing-header 401 via @PermitAll.

Changes:

  • Updated JwtAuthFilter to eagerly 401 on missing/non-Bearer headers and to 401 with error="invalid_token" on verification failures, with @PermitAll opting out of the missing-header 401.
  • Added UnauthorizedException + UnauthorizedExceptionMapper to translate auth failures into a 401 + WWW-Authenticate challenge.
  • Wired the mapper into the affected microservices and added unit tests for the new filter/mapper behavior; annotated the dataset cover endpoint as @PermitAll for anonymous public access.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala Marks dataset cover endpoint @PermitAll so anonymous callers can reach @Auth Optional[SessionUser] logic.
file-service/src/main/scala/org/apache/texera/service/FileService.scala Registers UnauthorizedExceptionMapper alongside JwtAuthFilter.
config-service/src/main/scala/org/apache/texera/service/ConfigService.scala Registers UnauthorizedExceptionMapper alongside JwtAuthFilter.
computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala Registers UnauthorizedExceptionMapper alongside JwtAuthFilter.
access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala Registers UnauthorizedExceptionMapper alongside JwtAuthFilter.
common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala Implements eager-401 behavior and @PermitAll opt-out for missing/non-Bearer headers.
common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.scala Adds a dedicated exception + JAX-RS exception mapper for 401 + challenge header responses.
common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala Adds unit coverage for missing/non-Bearer/invalid/valid token flows and @PermitAll semantics.
access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.scala Adds unit coverage ensuring mapper returns 401 and preserves challenge verbatim without an entity.
common/auth/build.sbt Adds jakarta.annotation-api dependency for @PermitAll.
amber/LICENSE-binary-java Updates binary license inventory to include jakarta.annotation-api.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +61 to +66
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
if (isPermitAll) return
throw new UnauthorizedException(JwtAuthFilter.BearerChallenge)
}

val token = authHeader.substring(7) // Remove "Bearer " prefix
* implementation. The companion [[UnauthorizedExceptionMapper]] converts
* the exception to the actual HTTP response at the JAX-RS edge.
*/
class UnauthorizedException(val challenge: String) extends RuntimeException(challenge)
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

3 participants