Skip to content

Improve JWT error handling and logging#2970

Merged
rrayst merged 13 commits into
masterfrom
jwt-interceptor-log-messages
Jun 30, 2026
Merged

Improve JWT error handling and logging#2970
rrayst merged 13 commits into
masterfrom
jwt-interceptor-log-messages

Conversation

@predic8

@predic8 predic8 commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes

    • JWT authentication now consistently returns HTTP 401 (Unauthorized) for missing tokens and JWT validation failures, including invalid signatures (instead of 400).
    • Improved error detail messaging for clearer guidance on JWT issues (e.g., missing token, invalid signature, malformed headers).
    • Refined JWT-related logs to better reflect retrieval and validation outcomes.
  • Tests

    • Updated JWT, OpenAPI/example, tutorial, and integration tests to expect 401 and to use more resilient prefix-based assertions for error details.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 800e2ba0-0ad6-4556-a49d-c8f2abc88cd7

📥 Commits

Reviewing files that changed from the base of the PR and between 87f9315 and 794ace8.

📒 Files selected for processing (2)
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/OAuth2Test.java

📝 Walkthrough

Walkthrough

JwtAuthInterceptor replaces wildcard imports with explicit ones, adds ERROR_JWT_INVALID_SIGNATURE, changes handleRequest to map JWT failures to ProblemDetails.security(...) with HTTP 401 and updated logs, logs null JWTs in handleJwt, and updates related tests to match the new responses.

Changes

JWT Error Handling Improvements

Layer / File(s) Summary
Imports and new error constant
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
Wildcard imports are replaced with explicit imports, and ERROR_JWT_INVALID_SIGNATURE is added.
JWT validation error response handling
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
handleRequest now maps JSON parsing, invalid signature, validation, and fallback JWT failures to ProblemDetails.security(...) responses with HTTP 401 and updated logging.
Null token diagnostic logging
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
handleJwt logs when no JWT is found before throwing JWTException.
Test imports and assertion adjustments
core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java, distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java, distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/ApiKeyToJwtTest.java, distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/IssuingAndValidatingJwtsTutorialTest.java, core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/JwksRefreshTest.java, core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/OAuth2Test.java
JWT-related tests switch to explicit imports where applicable and update expected status codes and error-detail checks to match the new 401 responses and signature/validation messages.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rrayst
  • christiangoerdes

Poem

🐰 I nibbled on a token bright,
Then leapt through 401 tonight.
A signature that went astray
Got logged, then hopped the other way.
Now JWTs hop clear and light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes to JWT exception handling and logging.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jwt-interceptor-log-messages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`:
- Line 131: The log call in JwtAuthInterceptor uses log.info("Could not retrieve
JWT: ", e.getMessage()) without a "{}" placeholder so the message is never
logged; update the log invocation in JwtAuthInterceptor (the log.info call) to
include a placeholder and/or pass the exception correctly, e.g. use
log.info("Could not retrieve JWT: {}", e.getMessage()) or log.info("Could not
retrieve JWT", e) to ensure the error message or stack trace is actually
recorded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e0edaa5-0999-49b6-a0f9-413924998e88

📥 Commits

Reviewing files that changed from the base of the PR and between ccef747 and f79fb77.

📒 Files selected for processing (1)
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java (2)

123-125: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid exposing raw JWT validation exception text to clients.

detail(ERROR_VALIDATION_FAILED + ": " + e.getMessage()) leaks internal validator output in the HTTP response. Keep client detail generic and retain specifics in logs only.

🔧 Proposed fix
         } catch (InvalidJwtException e) {
             log.info("JWT validation failed: {}", e.getErrorDetails());
             security(router.getConfiguration().isProduction(), "jwt-auth")
-                    .detail(ERROR_VALIDATION_FAILED + ": " + e.getMessage())
+                    .detail(ERROR_VALIDATION_FAILED)
                     .addSubSee(ERROR_VALIDATION_FAILED_ID)
                     .stacktrace(false)
                     .status(401)
                     .buildAndSetResponse(exc);
             return RETURN;

Based on learnings, HTTP ProblemDetails.detail(...) should avoid exposing internal implementation details to clients when a generic message is sufficient.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`
around lines 123 - 125, In JwtAuthInterceptor, the
security(...).detail(ERROR_VALIDATION_FAILED + ": " + e.getMessage()) call
exposes internal validator text to clients; change it to set a generic
client-facing detail (e.g. ERROR_VALIDATION_FAILED or a short message) and move
the full exception message into a server log (use logger.error or similar)
referencing the exception (e) so the detailed error is recorded but not
returned; update the invocation around
security(router.getConfiguration().isProduction(), "jwt-auth").detail(...) and
keep addSubSee(ERROR_VALIDATION_FAILED_ID) as-is.

Source: Learnings


96-101: ⚠️ Potential issue | 🔴 Critical

Propagate JWTException’s structured id into ProblemDetails (addSubSee)
JWTException exposes the structured error id via getId(), but JwtAuthInterceptor’s catch (JWTException e) branch (lines 96-101) only sets .detail(e.getMessage()) and never calls .addSubSee(e.getId()); this drops the IDs from thrown new JWTException(..., ERROR_JWT_NOT_FOUND_ID) / new JWTException(..., ERROR_UNKNOWN_KEY_ID) paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`
around lines 96 - 101, In JwtAuthInterceptor's catch (JWTException e) block,
include the exception's structured id into the ProblemDetails by calling
addSubSee(e.getId()) on the security(...) builder before
buildAndSetResponse(exc); specifically, after calling .detail(e.getMessage())
(and only if e.getId() is non-null/non-empty), invoke .addSubSee(e.getId()) on
the same builder chain so JWTException.getId() values (e.g.
ERROR_JWT_NOT_FOUND_ID / ERROR_UNKNOWN_KEY_ID) are propagated into the response.
🧹 Nitpick comments (1)
core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java (1)

117-121: ⚡ Quick win

Lock the new 401 contract with explicit status assertions.

These cases now validate detail prefixes but still only check isUserError(). Since this PR’s behavior change is specifically 401, assert status code explicitly to prevent silent regressions to other 4xx codes.

✅ Minimal test hardening
                     assertTrue(exc.getResponse().isUserError());
+                    assertEquals(401, exc.getResponse().getStatusCode());
                     assertNull(exc.getProperties().get("jwt"));
                     var detail = (String) unpackBody(exc).get("detail");
                     assertTrue(detail.startsWith(ERROR_JWT_INVALID_SIGNATURE));

Also applies to: 170-174, 186-190, 202-206

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java`
around lines 117 - 121, The tests in JwtAuthInterceptorTest currently only
assert exc.getResponse().isUserError(); update them to lock the 401 contract by
asserting the explicit HTTP status instead (e.g. assertEquals(401,
exc.getResponse().getStatus())) wherever the test checks isUserError() —
specifically in the block that verifies ERROR_JWT_INVALID_SIGNATURE (uses
unpackBody(exc) and variable exc) and the other similar blocks noted (lines
referenced in the review: 170-174, 186-190, 202-206); keep the existing checks
for null jwt and detail prefix assertions (ERROR_JWT_INVALID_SIGNATURE) and just
add the explicit status assertion to prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`:
- Around line 123-125: In JwtAuthInterceptor, the
security(...).detail(ERROR_VALIDATION_FAILED + ": " + e.getMessage()) call
exposes internal validator text to clients; change it to set a generic
client-facing detail (e.g. ERROR_VALIDATION_FAILED or a short message) and move
the full exception message into a server log (use logger.error or similar)
referencing the exception (e) so the detailed error is recorded but not
returned; update the invocation around
security(router.getConfiguration().isProduction(), "jwt-auth").detail(...) and
keep addSubSee(ERROR_VALIDATION_FAILED_ID) as-is.
- Around line 96-101: In JwtAuthInterceptor's catch (JWTException e) block,
include the exception's structured id into the ProblemDetails by calling
addSubSee(e.getId()) on the security(...) builder before
buildAndSetResponse(exc); specifically, after calling .detail(e.getMessage())
(and only if e.getId() is non-null/non-empty), invoke .addSubSee(e.getId()) on
the same builder chain so JWTException.getId() values (e.g.
ERROR_JWT_NOT_FOUND_ID / ERROR_UNKNOWN_KEY_ID) are propagated into the response.

---

Nitpick comments:
In
`@core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java`:
- Around line 117-121: The tests in JwtAuthInterceptorTest currently only assert
exc.getResponse().isUserError(); update them to lock the 401 contract by
asserting the explicit HTTP status instead (e.g. assertEquals(401,
exc.getResponse().getStatus())) wherever the test checks isUserError() —
specifically in the block that verifies ERROR_JWT_INVALID_SIGNATURE (uses
unpackBody(exc) and variable exc) and the other similar blocks noted (lines
referenced in the review: 170-174, 186-190, 202-206); keep the existing checks
for null jwt and detail prefix assertions (ERROR_JWT_INVALID_SIGNATURE) and just
add the explicit status assertion to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ed5c6ae-5931-4dad-8d7e-aea9970c84af

📥 Commits

Reviewing files that changed from the base of the PR and between a5e6ccc and 67853a9.

📒 Files selected for processing (2)
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
  • core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java

@predic8

predic8 commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

/ok-to-test

1 similar comment
@predic8

predic8 commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

/ok-to-test

@membrane-ci-server

Copy link
Copy Markdown

This pull request needs "/ok-to-test" from an authorized committer.

@rrayst

rrayst commented Jun 12, 2026

Copy link
Copy Markdown
Member

failing tests

@predic8 predic8 added this to the 7.3.0 milestone Jun 28, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/ApiKeyToJwtTest.java (1)

72-76: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Corrupt only the signature if you want to assert the signature-specific error.

Line 72 rewrites the entire compact JWT, so this can fail as malformed header/claims instead of reaching the JWT signature is invalid. branch. That makes the new assertion on Line 76 brittle and is a likely cause of the failing tests.

Proposed fix
 `@Test`
 void invalidJwt() {
+    String[] parts = TOKEN.split("\\.");
+    String tamperedSignature = (parts[2].charAt(0) == 'A' ? "B" : "A") + parts[2].substring(1);
+
     given()
-        .header(AUTHORIZATION, "Bearer " + TOKEN.replace("a", "d"))
+        .header(AUTHORIZATION, "Bearer " + parts[0] + "." + parts[1] + "." + tamperedSignature)
     .when()
         .get("http://localhost:2001")
     .then().assertThat()
         .body(containsString("JWT signature is invalid.")).statusCode(401);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/ApiKeyToJwtTest.java`
around lines 72 - 76, The JWT tampering in ApiKeyToJwtTest is changing more than
just the signature, so the request may fail as a malformed token instead of
triggering the signature validation path. Update the test setup around the TOKEN
usage to corrupt only the signature segment while keeping the header and claims
intact, so the assertion for JWT signature-specific handling remains stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/ApiKeyToJwtTest.java`:
- Around line 72-76: The JWT tampering in ApiKeyToJwtTest is changing more than
just the signature, so the request may fail as a malformed token instead of
triggering the signature validation path. Update the test setup around the TOKEN
usage to corrupt only the signature segment while keeping the header and claims
intact, so the assertion for JWT signature-specific handling remains stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5274f9bd-8b6e-446f-9305-fc98a1a74819

📥 Commits

Reviewing files that changed from the base of the PR and between 67853a9 and 87f9315.

📒 Files selected for processing (3)
  • distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/openapi/OpenApiJwtAuthExampleTest.java
  • distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/test/ApiKeyToJwtTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/IssuingAndValidatingJwtsTutorialTest.java

@rrayst rrayst merged commit 15f3efc into master Jun 30, 2026
5 checks passed
@rrayst rrayst deleted the jwt-interceptor-log-messages branch June 30, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants