Skip to content

Tutorials: OAuth2#3020

Merged
rrayst merged 29 commits into
masterfrom
tutorials-oauth2
Jul 4, 2026
Merged

Tutorials: OAuth2#3020
rrayst merged 29 commits into
masterfrom
tutorials-oauth2

Conversation

@predic8

@predic8 predic8 commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Updated OAuth2/JWT tutorials (basics, client credentials, password flow, token renewal, distributed validation, requesting tokens).
    • Enhanced OAuth2/JWT handling with configurable JWT issuer (iss), audience (aud), and scope claim parsing (scp/scope), plus resource and scope allowlist validation.
  • Bug Fixes
    • Improved JWKS startup resilience with on-demand loading and better behavior while keys aren’t available.
    • Improved refresh-token/session cleanup and propagated token claims during refresh.
  • Tests / Docs
    • Added and expanded integration coverage for the new tutorial flows and claim behavior.
  • Chores
    • Updated JVM XML entity limits and refreshed logging setup.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: aadb04da-ab2c-45f9-b61f-ba31f97ff9d2

📥 Commits

Reviewing files that changed from the base of the PR and between 7d43d94 and db93ba2.

📒 Files selected for processing (1)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java

📝 Walkthrough

Walkthrough

This PR updates OAuth2 token issuance and validation to support resource, scope, and issuer claims, adds matching tutorial and integration test coverage, updates JWT scope parsing and JWKS loading, and changes test JVM XML entity limits.

Changes

OAuth2/JWT behavior

Layer / File(s) Summary
Token issuance and session flow
core/.../oauth2/Client.java, core/.../oauth2/ParamNames.java, core/.../oauth2/request/ParameterizedRequest.java, core/.../oauth2/request/tokenrequest/CredentialsFlow.java, core/.../oauth2/request/tokenrequest/PasswordFlow.java, core/.../oauth2/request/tokenrequest/RefreshTokenFlow.java, core/.../oauth2/tokengenerators/*, core/.../oauth2/OAuth2AuthorizationServerInterceptor.java, core/.../oauth2/SessionFinder.java
Adds resource and scope client fields, validates token requests, carries aud/scope claims into tokens, supports issuer claims, logs refresh-token failures, and makes userDataProvider optional.
JWT validation and scope parsing
core/.../jwt/JwtAuthInterceptor.java, core/.../jwt/Jwks.java, core/.../jwt/HeaderJwtRetriever.java, core/.../tokenvalidation/OAuth2TokenValidatorInterceptor.java, core/.../security/JWTSecurityScheme.java
Adds expected issuer validation, retries JWKS loading on demand, logs missing headers and validation failures, and reads scopes from scp or scope.

Tutorials, documentation, and integration tests

Layer / File(s) Summary
Tutorial test coverage
core/src/test/.../oauth2/*, core/src/test/.../jwt/JwtAuthInterceptorTest.java, core/src/test/.../security/JWTSecuritySchemeTest.java, distribution/src/test/.../security/*
Adds coverage for issuer validation, scope parsing, optional user-data-provider behavior, resource/scope allowlists, password-flow claim propagation, and new tutorial scenarios.
Tutorial YAML and markdown
distribution/tutorials/security/*, distribution/src/test/.../security/*, distribution/tutorials/security/README.md
Adds new OAuth2/JWT tutorial YAML files and matching tests, updates JWT tutorial docs and README links, and adds the JWT request tutorial markdown.
Release notes
docs/RELEASE_NOTES.md, distribution/release-notes/*.md
Updates the release notes content and removes placeholder release-note text from distribution files.

Build, example, and misc updates

Layer / File(s) Summary
Build and example updates
core/pom.xml, distribution/pom.xml, pom.xml, war/pom.xml, distribution/src/test/.../OAuth2APIExampleTest.java, core/src/main/java/.../SessionManager.java, docs/ROADMAP.md
Adjusts XML entity limits in test JVM args, updates an example test wait condition, initializes session timestamps at creation, and adds blank spacing in the roadmap.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • membrane/api-gateway#2616: Adds related OAuth2 security tutorial test infrastructure used by the new distribution tests here.
  • membrane/api-gateway#2130: Covers JWT claim behavior in refreshed access tokens, which matches the refreshed-claims checks added here.
  • membrane/api-gateway#1830: Shares the JWT scope-claim parsing behavior tested by the updated JWTSecurityScheme coverage.

Suggested reviewers: rrayst

Poem

A bunny hops through claims and keys,
With scopes and issuers in the breeze.
JWKS wakes up when caches rest,
Fresh tokens pass each little test.
I nibble docs and thump the floor —
This gateway tale has plenty more. 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic to convey the main change set, so it’s not specific enough for a commit history scan. Use a specific title that names the main OAuth2 tutorial additions, such as adding OAuth2 tutorials and related tests.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 tutorials-oauth2
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch tutorials-oauth2

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

🧹 Nitpick comments (1)
distribution/tutorials/security/50-OAuth2-Client-Credentials.yaml (1)

34-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the end-user credential store from this client-credentials example.

The tutorial text says this flow is machine-to-machine with no username/password, but Lines 34-37 still provision a user store. Even if the test never uses it, that makes the example teach the wrong contract for client_credentials.

Suggested cleanup
     - oauth2authserver:
         issuer: http://localhost:7007
-        staticUserDataProvider:
-          users:
-            - username: john
-              password: password
         staticClientList:
           clients:
             - clientId: abc
🤖 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/tutorials/security/50-OAuth2-Client-Credentials.yaml` around
lines 34 - 37, The OAuth2 client-credentials tutorial still provisions an
end-user login store via staticUserDataProvider, which conflicts with the
machine-to-machine contract. Remove the users/password block from the example
YAML so the 50-OAuth2-Client-Credentials tutorial only demonstrates
client_credentials setup and no username/password authentication.
🤖 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 `@distribution/tutorials/security/55-OAuth2-Client-Token-Renewal.yaml`:
- Around line 22-50: The OAuth2 client renewal tutorial currently never
exercises token expiry, so it only validates initial acquisition. Update the
OAuth2 auth-server setup in the tutorial config so the access token lifetime is
intentionally short, and adjust OAuth2ClientTokenRenewalTutorialTest to send a
second request after the first token has expired. Use the existing
oauth2authserver and oauth2Client flow blocks as the places to change the
renewal scenario, and keep the test asserting that the client re-fetches a new
token on the second request.

---

Nitpick comments:
In `@distribution/tutorials/security/50-OAuth2-Client-Credentials.yaml`:
- Around line 34-37: The OAuth2 client-credentials tutorial still provisions an
end-user login store via staticUserDataProvider, which conflicts with the
machine-to-machine contract. Remove the users/password block from the example
YAML so the 50-OAuth2-Client-Credentials tutorial only demonstrates
client_credentials setup and no username/password authentication.
🪄 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: 36651a06-e4c2-43b6-ad6e-3489dd6aa35b

📥 Commits

Reviewing files that changed from the base of the PR and between d7e3dfa and 3951855.

📒 Files selected for processing (6)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientCredentialsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2PasswordFlowTutorialTest.java
  • distribution/tutorials/security/40-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/50-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/55-OAuth2-Client-Token-Renewal.yaml

Comment thread distribution/tutorials/security/55-OAuth2-Client-Token-Renewal.yaml Outdated
@predic8 predic8 added this to the 7.3.0 milestone Jun 28, 2026
@membrane-ci-server

Copy link
Copy Markdown

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

@rrayst

rrayst commented Jun 28, 2026

Copy link
Copy Markdown
Member

/ok-to-test

@rrayst

rrayst commented Jun 28, 2026

Copy link
Copy Markdown
Member

OAuth2ClientTokenRenewalTutorialTest.gatewayRenewsTokenAfterExpiry test failure

@predic8 predic8 requested a review from rrayst June 29, 2026 06:06

@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: 3

🤖 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
`@distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java`:
- Around line 87-108: The OAuth2 token renewal test is flaky because
`OAuth2ClientTokenRenewalTutorialTest` relies on fixed `Thread.sleep` timing
around the cache expiry boundary. Replace the sleep-based assertions in
`gatewayRenewsTokenAfterExpiry`/`reusesCachedTokenThenRenewsAfterExpiry` with a
polling wait that checks `countTokenFetches()` and `lastForwardedSuffix()` until
the expected state is reached, or otherwise widen the timing margins
significantly. Keep the checks tied to the existing helper methods so the test
validates cached reuse first and token renewal only after expiry, without
depending on exact scheduler timing.

In `@distribution/tutorials/security/50-OAuth2-Token-Validation.yaml`:
- Around line 61-64: The tutorial comment in the OAuth2 token validation YAML
points to the wrong line for the tokenValidator endpoint. Update the explanatory
text near the token validation step to reference the actual `endpoint:` setting
in the `tokenValidator` block, using the correct location relative to the
`target:` and `endpoint:` keys so readers are directed to the right field.

In `@distribution/tutorials/security/README.md`:
- Around line 12-24: The tutorial index in the security README is missing the
new JWKS flow, so the added 42a/42b tutorials are not discoverable. Update the
list under the tutorial overview and the “Next Steps” section to include the new
JWKS issuer and validation tutorials alongside 40 and 41. Use the existing
tutorial entry style in the README and reference the new filenames
42a-JWKS-Issuer.yaml and 42b-JWKS-Validation.yaml when adding the step 3
guidance.
🪄 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: a555bcb1-485a-42eb-acef-bcb1c6b7d5b1

📥 Commits

Reviewing files that changed from the base of the PR and between 6c81a1e and 7fd0e0d.

📒 Files selected for processing (17)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientCredentialsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2PasswordFlowTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2TokenValidationTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/IssuingAndValidatingJwtsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/JwksIssuerAndValidationTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/40-JWT-Requesting-Token.md
  • distribution/tutorials/security/41-JWT-Issuing-and-Validating.yaml
  • distribution/tutorials/security/42a-JWKS-Issuer.yaml
  • distribution/tutorials/security/42b-JWKS-Validation.yaml
  • distribution/tutorials/security/50-OAuth2-Token-Validation.yaml
  • distribution/tutorials/security/51-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/52-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/tutorials/security/README.md
  • docs/ROADMAP.md
✅ Files skipped from review due to trivial changes (2)
  • distribution/tutorials/security/42a-JWKS-Issuer.yaml
  • distribution/tutorials/security/42b-JWKS-Validation.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientCredentialsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2PasswordFlowTutorialTest.java

Comment thread distribution/tutorials/security/50-OAuth2-Token-Validation.yaml Outdated
Comment thread distribution/tutorials/security/README.md Outdated
@rrayst

rrayst commented Jul 1, 2026

Copy link
Copy Markdown
Member

test failure

@predic8 predic8 marked this pull request as draft July 1, 2026 18:37
predic8 and others added 6 commits July 2, 2026 08:15
Client credentials grant:
- New <client> attribute "resources" (allowlist, RFC 8707): the "resource"
  request parameter is validated against it (error "invalid_target"),
  granted resources become the "aud" claim. Without the parameter, the
  whole allowlist is granted.
- New <client> attribute "scopes" (allowlist): the "scope" request
  parameter is validated against it (error "invalid_scope"), granted
  scopes become the "scope" claim (RFC 9068) and the response scope
  field. Without the attribute, scope is passed through unchecked.
- Client lookup and grant type check now run before the token is
  created; previously a token was created and registered even for
  invalid grant types.

Password grant:
- The "scopes" user attribute is passed into the token as "scopes"
  claim, analogous to the existing "aud" user attribute.

Both survive a refresh via i-* claims in the refresh token. Requires a
JWT token generator (bearerJwtToken); opaque tokens have no claims.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RFC 9068 requires access tokens to carry an "iss" claim; so far the
configured issuer only appeared in the discovery document and in ID
tokens. The authorization server now passes its issuer to the access
and refresh token generators, and bearerJwtToken writes it into every
token. Opaque bearerToken ignores it (no claims to carry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pure client_credentials setup has no users, yet the config forced an
empty <staticUserDataProvider/> placeholder. The attribute is no longer
@required; without it, startup logs a prominent warning (same pattern
as missing location/consentFile), the login view is disabled, and
user-based flows answer with "access_denied".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rejections that were previously silent now log at info level: invalid
or session-less tokens at the userinfo endpoint, unresolvable refresh
tokens, tokenValidator failures, missing JWT headers, and JWTs signed
by unknown keys. jwtAuth error responses now include the concrete
jose4j validation failure in the detail field; test assertions check
the detailed messages.

Also fixes a regression introduced with the userinfo log split: the
session lookup called getToken() before isValid(), so a malformed
Authorization header caused a 500 instead of 401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
If set, the token's "iss" claim must be present and match, mirroring
the existing expectedAud/expectedTid checks. Complements the iss claim
now issued by the oauth2authserver's bearerJwtToken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
predic8 and others added 3 commits July 2, 2026 14:52
The security scheme only looked at the "scp" claim (Microsoft Entra ID
style), so scopes() and scope checks stayed empty for tokens issued by
Membrane's own authorization server, which uses the standard "scope"
claim. "scope" is now the fallback when "scp" is absent; "scp" still
wins when both are present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jwtAuth no longer fails hard when the JWKS URIs cannot be resolved at
init time (e.g. the issuer starts after the validator). The gateway
starts, logs the failure, and fetches the keys on the first request
that needs them. Until then, requests validated against the key set
are rejected. Periodic refresh keeps the previous key set when a
refresh returns no keys. Tutorial 42b no longer claims the issuer must
run first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@predic8 predic8 marked this pull request as ready for review July 2, 2026 17:12
predic8 added 3 commits July 2, 2026 19:13
… updated flows and tests

Consolidated and renamed tutorials for clarity, aligning with current OAuth2 standards and best practices. Replaced redundant files with streamlined examples, updated JWT claim handling, and adjusted tests for new configurations.
…sweep

Added a new test to confirm that refresh tokens remain valid after a cleanup sweep in session management. Updated OAuth2 tutorials to better illustrate refresh token usage and cleanup behavior. Adjusted session initialization to prevent premature token invalidation.
@predic8

predic8 commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai resume reviews

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@predic8 Resuming automatic reviews for this PR.

✅ Action performed

Reviews resumed.

@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: 4

🧹 Nitpick comments (4)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java (1)

51-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reasonable validation split with clear logging.

Checking isValid() before getToken() correctly avoids the exception path on malformed headers, and the two distinct log messages aid debuggability without leaking token contents.

One minor nit: authHeader.getToken() is called three times (lines 56, 60, 62) after validity is confirmed. Caching it in a local variable once would be slightly cleaner, though not a correctness concern.

♻️ Optional cleanup
-        if(!authServer.getSessionFinder().hasSessionForToken(authHeader.getToken())) {
+        String token = authHeader.getToken();
+        if(!authServer.getSessionFinder().hasSessionForToken(token)) {
             log.info("Access token at userinfo endpoint not accepted: token is valid but has no associated session.");
             return buildWwwAuthenticateErrorResponse( Response.unauthorized(), "invalid_token");
         }
-        sessionProperties = new HashMap<>(authServer.getSessionFinder().getSessionForToken(authHeader.getToken()).getUserAttributes());
-
-        String token = authHeader.getToken();
+        sessionProperties = new HashMap<>(authServer.getSessionFinder().getSessionForToken(token).getUserAttributes());
🤖 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/oauth2/request/UserinfoRequest.java`
around lines 51 - 64, Cache the result of authHeader.getToken() in
UserinfoRequest after the isValid() check instead of calling it multiple times.
Use the local token variable for the hasSessionForToken/getSessionForToken
lookup and for getUsername, keeping the existing validation order and log
messages intact.
distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml (1)

33-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider demonstrating expectedIss given this PR's new issuer-validation feature.

This same PR adds expectedIss support to jwtAuth (see JwtAuthInterceptor.createValidator), and 54a explicitly sets an issuer and emits an iss claim, yet this validator doesn't configure expectedIss to check it. For a tutorial specifically about cross-instance trust between issuer and validator, showing expectedIss: http://localhost:7007 would better demonstrate/exercise the just-added feature and improve the security posture of the example.

Suggested addition
     - jwtAuth:
         jwks:
           jwksUris: http://localhost:7007/oauth2/certs
+        expectedIss: http://localhost:7007
🤖 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/tutorials/security/54b-OAuth2-Distributed-Validation.yaml`
around lines 33 - 35, The `jwtAuth` example in the distributed validation
tutorial is missing the new issuer check, so update the tutorial YAML to
demonstrate `expectedIss` alongside `jwksUris`. Use the `jwtAuth` configuration
in this tutorial to add `expectedIss: http://localhost:7007`, matching the
issuer shown in the paired 54a example and exercising
`JwtAuthInterceptor.createValidator`’s new issuer-validation support.
distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java (1)

58-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Test only covers issuer-first startup; consider adding the reversed-order case.

The class doc explains that The JWTS URIs may be unreachable during init() (e.g. Membrane starts before the issuer); init() only logs that failure, so the load is retried here on first use. The tutorial's own comments (54b, lines 31-32) explicitly advertise that the validator "still starts and fetches the keys on the first request" if the issuer isn't up yet — but no test exercises that retry path (starting the validator before the issuer). Adding a second test case would give coverage for the documented resiliency behavior this tutorial highlights.

🤖 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/tutorials/security/OAuth2DistributedValidationTutorialTest.java`
around lines 58 - 91, Add coverage for the retry path where the JWT validator
starts before the issuer, since the current
OAuth2DistributedValidationTutorialTest only verifies issuer-first startup.
Introduce a second test in this class that brings up the validator/protected API
first, confirms the initial key fetch can fail during init(), then starts the
issuer and asserts the first authenticated request still succeeds by fetching
JWKS on demand. Use the existing issuesJwtAndValidatesViaJwks flow and the
tutorial’s documented startup order behavior as the reference point.
core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java (1)

89-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared test scaffolding across new OAuth2 tests.

The tokenRequest() helper, BearerJwtTokenGenerator/RefreshTokenConfig wiring, and license header are duplicated near-verbatim in CredentialsFlowScopeTest, PasswordFlowClaimsTest, and (per graph context) CredentialsFlowResourceTest. Consider extracting a shared abstract base test class (e.g. OAuth2TokenRequestTestBase) with tokenRequest()/accessTokenClaims() and common oasi setup, to avoid drift as more OAuth2 tutorial tests are added.

🤖 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/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java`
around lines 89 - 100, The OAuth2 test setup is duplicated across the new flow
tests, so extract the shared scaffolding into a common abstract base such as
OAuth2TokenRequestTestBase. Move the repeated tokenRequest() helper, the common
oasi initialization, and the BearerJwtTokenGenerator/RefreshTokenConfig wiring
into that base, then have CredentialsFlowScopeTest, PasswordFlowClaimsTest, and
CredentialsFlowResourceTest inherit from it and keep only test-specific
assertions like accessTokenClaims(). Also centralize the shared license header
there so the tests stay consistent and easier to maintain.
🤖 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/Jwks.java`:
- Around line 111-125: The on-demand JWKS refresh in Jwks.reloadJwksIfNeeded()
is unsynchronized, so concurrent requests can all enter loadJwks(false) when
jwks is empty and hammer the JWKS endpoint. Add coordination in
reloadJwksIfNeeded()/getJwks() (for example a lock plus a last-attempt
timestamp/backoff) so only one request thread performs the blocking reload and
rapid failures do not trigger repeated network fetches on every call.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`:
- Around line 131-140: The catch-all in JwtAuthInterceptor should not expose raw
exception messages to clients; it currently passes e.getMessage() into the
response detail, which can leak internal JWKS/config information. Update this
exception path to use a fixed generic detail string like the other
JwtAuthInterceptor handlers, keep the specific e.getMessage() only in the server
log, and leave the response construction in
security(...).detail(...).addSubSee(...) consistent with the existing JWT error
handling.

In `@distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml`:
- Around line 20-26: The tutorial text and the JWT claims configuration are
inconsistent: the walkthrough mentions noting `clientId`, but `claims.value` in
the distributed issuer setup only emits `sub` and `iss`. Update either the
comment in the tutorial section or the claims definition so they match, and use
the existing `claims.value`/JWT walkthrough wording to keep the step-by-step
instructions aligned with the actual token contents.

In `@docs/ROADMAP.md`:
- Line 123: The sample command in the roadmap still references the old tutorial
filename, so update the example in the roadmap content to use the new
`54b-OAuth2-Distributed-Validation.yaml` name instead of
`54b-OpenID-Connect-Login.yaml`. Make the change in the existing command snippet
so readers copying it will run the correct tutorial file.

---

Nitpick comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java`:
- Around line 51-64: Cache the result of authHeader.getToken() in
UserinfoRequest after the isValid() check instead of calling it multiple times.
Use the local token variable for the hasSessionForToken/getSessionForToken
lookup and for getUsername, keeping the existing validation order and log
messages intact.

In
`@core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java`:
- Around line 89-100: The OAuth2 test setup is duplicated across the new flow
tests, so extract the shared scaffolding into a common abstract base such as
OAuth2TokenRequestTestBase. Move the repeated tokenRequest() helper, the common
oasi initialization, and the BearerJwtTokenGenerator/RefreshTokenConfig wiring
into that base, then have CredentialsFlowScopeTest, PasswordFlowClaimsTest, and
CredentialsFlowResourceTest inherit from it and keep only test-specific
assertions like accessTokenClaims(). Also centralize the shared license header
there so the tests stay consistent and easier to maintain.

In
`@distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java`:
- Around line 58-91: Add coverage for the retry path where the JWT validator
starts before the issuer, since the current
OAuth2DistributedValidationTutorialTest only verifies issuer-first startup.
Introduce a second test in this class that brings up the validator/protected API
first, confirms the initial key fetch can fail during init(), then starts the
issuer and asserts the first authenticated request still succeeds by fetching
JWKS on demand. Use the existing issuesJwtAndValidatesViaJwks flow and the
tutorial’s documented startup order behavior as the reference point.

In `@distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml`:
- Around line 33-35: The `jwtAuth` example in the distributed validation
tutorial is missing the new issuer check, so update the tutorial YAML to
demonstrate `expectedIss` alongside `jwksUris`. Use the `jwtAuth` configuration
in this tutorial to add `expectedIss: http://localhost:7007`, matching the
issuer shown in the paired 54a example and exercising
`JwtAuthInterceptor.createValidator`’s new issuer-validation support.
🪄 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: 628073c5-8cf6-4eb9-ad38-581849238ecd

📥 Commits

Reviewing files that changed from the base of the PR and between 6c81a1e and 1d40912.

📒 Files selected for processing (38)
  • core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/HeaderJwtRetriever.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2AuthorizationServerInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ParamNames.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/ParameterizedRequest.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/CredentialsFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/TokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokenvalidation/OAuth2TokenValidatorInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/security/JWTSecurityScheme.java
  • core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java
  • core/src/test/java/com/predic8/membrane/core/security/JWTSecuritySchemeTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/CredentialsFlowResourceTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/CredentialsFlowScopeTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/PasswordFlowClaimsTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2BasicsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientCredentialsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2PasswordFlowTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/JwtSigningTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/40-JWT-Requesting-Token.md
  • distribution/tutorials/security/41-JWT-Signing.yaml
  • distribution/tutorials/security/50-OAuth2-Basics.yaml
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/52-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml
  • distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml
  • distribution/tutorials/security/README.md
  • docs/ROADMAP.md
✅ Files skipped from review due to trivial changes (4)
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokenvalidation/OAuth2TokenValidatorInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
  • distribution/tutorials/security/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.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

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (4)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java (1)

51-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reasonable validation split with clear logging.

Checking isValid() before getToken() correctly avoids the exception path on malformed headers, and the two distinct log messages aid debuggability without leaking token contents.

One minor nit: authHeader.getToken() is called three times (lines 56, 60, 62) after validity is confirmed. Caching it in a local variable once would be slightly cleaner, though not a correctness concern.

♻️ Optional cleanup
-        if(!authServer.getSessionFinder().hasSessionForToken(authHeader.getToken())) {
+        String token = authHeader.getToken();
+        if(!authServer.getSessionFinder().hasSessionForToken(token)) {
             log.info("Access token at userinfo endpoint not accepted: token is valid but has no associated session.");
             return buildWwwAuthenticateErrorResponse( Response.unauthorized(), "invalid_token");
         }
-        sessionProperties = new HashMap<>(authServer.getSessionFinder().getSessionForToken(authHeader.getToken()).getUserAttributes());
-
-        String token = authHeader.getToken();
+        sessionProperties = new HashMap<>(authServer.getSessionFinder().getSessionForToken(token).getUserAttributes());
🤖 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/oauth2/request/UserinfoRequest.java`
around lines 51 - 64, Cache the result of authHeader.getToken() in
UserinfoRequest after the isValid() check instead of calling it multiple times.
Use the local token variable for the hasSessionForToken/getSessionForToken
lookup and for getUsername, keeping the existing validation order and log
messages intact.
distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml (1)

33-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Consider demonstrating expectedIss given this PR's new issuer-validation feature.

This same PR adds expectedIss support to jwtAuth (see JwtAuthInterceptor.createValidator), and 54a explicitly sets an issuer and emits an iss claim, yet this validator doesn't configure expectedIss to check it. For a tutorial specifically about cross-instance trust between issuer and validator, showing expectedIss: http://localhost:7007 would better demonstrate/exercise the just-added feature and improve the security posture of the example.

Suggested addition
     - jwtAuth:
         jwks:
           jwksUris: http://localhost:7007/oauth2/certs
+        expectedIss: http://localhost:7007
🤖 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/tutorials/security/54b-OAuth2-Distributed-Validation.yaml`
around lines 33 - 35, The `jwtAuth` example in the distributed validation
tutorial is missing the new issuer check, so update the tutorial YAML to
demonstrate `expectedIss` alongside `jwksUris`. Use the `jwtAuth` configuration
in this tutorial to add `expectedIss: http://localhost:7007`, matching the
issuer shown in the paired 54a example and exercising
`JwtAuthInterceptor.createValidator`’s new issuer-validation support.
distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java (1)

58-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Test only covers issuer-first startup; consider adding the reversed-order case.

The class doc explains that The JWTS URIs may be unreachable during init() (e.g. Membrane starts before the issuer); init() only logs that failure, so the load is retried here on first use. The tutorial's own comments (54b, lines 31-32) explicitly advertise that the validator "still starts and fetches the keys on the first request" if the issuer isn't up yet — but no test exercises that retry path (starting the validator before the issuer). Adding a second test case would give coverage for the documented resiliency behavior this tutorial highlights.

🤖 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/tutorials/security/OAuth2DistributedValidationTutorialTest.java`
around lines 58 - 91, Add coverage for the retry path where the JWT validator
starts before the issuer, since the current
OAuth2DistributedValidationTutorialTest only verifies issuer-first startup.
Introduce a second test in this class that brings up the validator/protected API
first, confirms the initial key fetch can fail during init(), then starts the
issuer and asserts the first authenticated request still succeeds by fetching
JWKS on demand. Use the existing issuesJwtAndValidatesViaJwks flow and the
tutorial’s documented startup order behavior as the reference point.
core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java (1)

89-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared test scaffolding across new OAuth2 tests.

The tokenRequest() helper, BearerJwtTokenGenerator/RefreshTokenConfig wiring, and license header are duplicated near-verbatim in CredentialsFlowScopeTest, PasswordFlowClaimsTest, and (per graph context) CredentialsFlowResourceTest. Consider extracting a shared abstract base test class (e.g. OAuth2TokenRequestTestBase) with tokenRequest()/accessTokenClaims() and common oasi setup, to avoid drift as more OAuth2 tutorial tests are added.

🤖 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/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java`
around lines 89 - 100, The OAuth2 test setup is duplicated across the new flow
tests, so extract the shared scaffolding into a common abstract base such as
OAuth2TokenRequestTestBase. Move the repeated tokenRequest() helper, the common
oasi initialization, and the BearerJwtTokenGenerator/RefreshTokenConfig wiring
into that base, then have CredentialsFlowScopeTest, PasswordFlowClaimsTest, and
CredentialsFlowResourceTest inherit from it and keep only test-specific
assertions like accessTokenClaims(). Also centralize the shared license header
there so the tests stay consistent and easier to maintain.
🤖 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/Jwks.java`:
- Around line 111-125: The on-demand JWKS refresh in Jwks.reloadJwksIfNeeded()
is unsynchronized, so concurrent requests can all enter loadJwks(false) when
jwks is empty and hammer the JWKS endpoint. Add coordination in
reloadJwksIfNeeded()/getJwks() (for example a lock plus a last-attempt
timestamp/backoff) so only one request thread performs the blocking reload and
rapid failures do not trigger repeated network fetches on every call.

In
`@core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java`:
- Around line 131-140: The catch-all in JwtAuthInterceptor should not expose raw
exception messages to clients; it currently passes e.getMessage() into the
response detail, which can leak internal JWKS/config information. Update this
exception path to use a fixed generic detail string like the other
JwtAuthInterceptor handlers, keep the specific e.getMessage() only in the server
log, and leave the response construction in
security(...).detail(...).addSubSee(...) consistent with the existing JWT error
handling.

In `@distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml`:
- Around line 20-26: The tutorial text and the JWT claims configuration are
inconsistent: the walkthrough mentions noting `clientId`, but `claims.value` in
the distributed issuer setup only emits `sub` and `iss`. Update either the
comment in the tutorial section or the claims definition so they match, and use
the existing `claims.value`/JWT walkthrough wording to keep the step-by-step
instructions aligned with the actual token contents.

In `@docs/ROADMAP.md`:
- Line 123: The sample command in the roadmap still references the old tutorial
filename, so update the example in the roadmap content to use the new
`54b-OAuth2-Distributed-Validation.yaml` name instead of
`54b-OpenID-Connect-Login.yaml`. Make the change in the existing command snippet
so readers copying it will run the correct tutorial file.

---

Nitpick comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java`:
- Around line 51-64: Cache the result of authHeader.getToken() in
UserinfoRequest after the isValid() check instead of calling it multiple times.
Use the local token variable for the hasSessionForToken/getSessionForToken
lookup and for getUsername, keeping the existing validation order and log
messages intact.

In
`@core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java`:
- Around line 89-100: The OAuth2 test setup is duplicated across the new flow
tests, so extract the shared scaffolding into a common abstract base such as
OAuth2TokenRequestTestBase. Move the repeated tokenRequest() helper, the common
oasi initialization, and the BearerJwtTokenGenerator/RefreshTokenConfig wiring
into that base, then have CredentialsFlowScopeTest, PasswordFlowClaimsTest, and
CredentialsFlowResourceTest inherit from it and keep only test-specific
assertions like accessTokenClaims(). Also centralize the shared license header
there so the tests stay consistent and easier to maintain.

In
`@distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java`:
- Around line 58-91: Add coverage for the retry path where the JWT validator
starts before the issuer, since the current
OAuth2DistributedValidationTutorialTest only verifies issuer-first startup.
Introduce a second test in this class that brings up the validator/protected API
first, confirms the initial key fetch can fail during init(), then starts the
issuer and asserts the first authenticated request still succeeds by fetching
JWKS on demand. Use the existing issuesJwtAndValidatesViaJwks flow and the
tutorial’s documented startup order behavior as the reference point.

In `@distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml`:
- Around line 33-35: The `jwtAuth` example in the distributed validation
tutorial is missing the new issuer check, so update the tutorial YAML to
demonstrate `expectedIss` alongside `jwksUris`. Use the `jwtAuth` configuration
in this tutorial to add `expectedIss: http://localhost:7007`, matching the
issuer shown in the paired 54a example and exercising
`JwtAuthInterceptor.createValidator`’s new issuer-validation support.
🪄 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: 628073c5-8cf6-4eb9-ad38-581849238ecd

📥 Commits

Reviewing files that changed from the base of the PR and between 6c81a1e and 1d40912.

📒 Files selected for processing (38)
  • core/src/main/java/com/predic8/membrane/core/interceptor/authentication/session/SessionManager.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/HeaderJwtRetriever.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/Client.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2AuthorizationServerInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ParamNames.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/ParameterizedRequest.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/UserinfoRequest.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/CredentialsFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/TokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokenvalidation/OAuth2TokenValidatorInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/security/JWTSecurityScheme.java
  • core/src/test/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptorTest.java
  • core/src/test/java/com/predic8/membrane/core/security/JWTSecuritySchemeTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/AuthServerWithoutUserDataProviderTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/CredentialsFlowResourceTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/CredentialsFlowScopeTest.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/PasswordFlowClaimsTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2BasicsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientCredentialsTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2DistributedValidationTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2PasswordFlowTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/JwtSigningTutorialTest.java
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/40-JWT-Requesting-Token.md
  • distribution/tutorials/security/41-JWT-Signing.yaml
  • distribution/tutorials/security/50-OAuth2-Basics.yaml
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/52-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml
  • distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml
  • distribution/tutorials/security/README.md
  • docs/ROADMAP.md
✅ Files skipped from review due to trivial changes (4)
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokenvalidation/OAuth2TokenValidatorInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
  • distribution/tutorials/security/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/OAuth2ClientTokenRenewalTutorialTest.java
🛑 Comments failed to post (4)
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java (1)

111-125: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unsynchronized on-demand JWKS reload can cause a request-thread thundering herd.

reloadJwksIfNeeded() is invoked on every getKeyByKid()/getJwks() call while jwks is empty, and performs a synchronous, blocking HTTP fetch with no cooldown or synchronization. If the JWKS URI stays unreachable, every incoming request needing validation re-attempts the fetch on its own request thread — a thundering herd against a struggling auth server, and added latency/timeout risk per request. The check-then-act on the jwks field is also not atomic, so concurrent requests can race into loadJwks() simultaneously.

Consider adding a lightweight last-attempt timestamp/backoff (or a lock) so concurrent/rapid retries don't all hit the network simultaneously while the source is down.

Also applies to: 149-149

🤖 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/Jwks.java`
around lines 111 - 125, The on-demand JWKS refresh in Jwks.reloadJwksIfNeeded()
is unsynchronized, so concurrent requests can all enter loadJwks(false) when
jwks is empty and hammer the JWKS endpoint. Add coordination in
reloadJwksIfNeeded()/getJwks() (for example a lock plus a last-attempt
timestamp/backoff) so only one request thread performs the blocking reload and
rapid failures do not trigger repeated network fetches on every call.
core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java (1)

131-140: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Generic exception handler now leaks raw internal messages to clients.

The catch-all now returns e.getMessage() as the response detail instead of a fixed string. This branch catches any unclassified exception — including failures propagated from Jwks.getKeyByKid()/reloadJwksIfNeeded(), whose RuntimeException/ConfigurationException messages embed the configured jwksUris (see Jwks.java's parseJwksUriIntoList, e.g. "Could not retrieve JWK keys from %s."). That means an unauthenticated client hitting this endpoint while the JWKS/auth server is down could receive the internal JWKS URL in the 401 response body. It's also fragile for exceptions with a null message (e.g. NPEs), and addSubSee(ERROR_JWT_NOT_FOUND_ID) stays hard-coded even though the underlying failure may be unrelated to "JWT not found."

Consider keeping a generic, fixed detail message for this catch-all (logging e.getMessage() server-side only), similar to how the other catch blocks use fixed constants.

🔒 Suggested fix
         } catch (Exception e) {
             log.info("Could not retrieve JWT: {}", e.getMessage());
             security(router.getConfiguration().isProduction(), "jwt-auth")
-                    .detail(e.getMessage())
+                    .detail(ERROR_JWT_NOT_FOUND)
                     .addSubSee(ERROR_JWT_NOT_FOUND_ID)
                     .stacktrace(true)
                     .status(401)
                     .buildAndSetResponse(exc);
             return RETURN;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        } catch (Exception e) {
            log.info("Could not retrieve JWT: {}", e.getMessage());
            security(router.getConfiguration().isProduction(), "jwt-auth")
                    .detail(ERROR_JWT_NOT_FOUND)
                    .addSubSee(ERROR_JWT_NOT_FOUND_ID)
                    .stacktrace(true)
                    .status(401)
                    .buildAndSetResponse(exc);
            return RETURN;
        }
🤖 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 131 - 140, The catch-all in JwtAuthInterceptor should not expose
raw exception messages to clients; it currently passes e.getMessage() into the
response detail, which can leak internal JWKS/config information. Update this
exception path to use a fixed generic detail string like the other
JwtAuthInterceptor handlers, keep the specific e.getMessage() only in the server
log, and leave the response construction in
security(...).detail(...).addSubSee(...) consistent with the existing JWT error
handling.
distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml (1)

20-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Doc/config mismatch: clientId mentioned but not in claims.value.

The walkthrough comment says to "Note sub, clientId, iss, exp and the kid "membrane" in the header." but claims.value (Line 56) only lists sub iss. Following the tutorial's own instructions, a reader inspecting the decoded token at jwt.io won't find a clientId claim, which is confusing for a tutorial meant to be followed step-by-step.

Suggested fix (align comment or config)
-        claims:
-          value: sub iss
+        claims:
+          value: sub iss clientId

or update the comment to drop the clientId reference if it isn't meant to be emitted.

Also applies to: 55-56

🤖 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/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml` around
lines 20 - 26, The tutorial text and the JWT claims configuration are
inconsistent: the walkthrough mentions noting `clientId`, but `claims.value` in
the distributed issuer setup only emits `sub` and `iss`. Update either the
comment in the tutorial section or the claims definition so they match, and use
the existing `claims.value`/JWT walkthrough wording to keep the step-by-step
instructions aligned with the actual token contents.
docs/ROADMAP.md (1)

123-123: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the sample command to the new tutorial filename.

The example still points at 54b-OpenID-Connect-Login.yaml, but this PR introduces 54b-OAuth2-Distributed-Validation.yaml instead. Readers copying the command will hit a missing file unless this is intentionally referencing an older tutorial.

Fix
-❯ ./membrane.sh -c 54b-OpenID-Connect-Login.yaml
+❯ ./membrane.sh -c 54b-OAuth2-Distributed-Validation.yaml
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

❯ ./membrane.sh -c 54b-OAuth2-Distributed-Validation.yaml
🤖 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 `@docs/ROADMAP.md` at line 123, The sample command in the roadmap still
references the old tutorial filename, so update the example in the roadmap
content to use the new `54b-OAuth2-Distributed-Validation.yaml` name instead of
`54b-OpenID-Connect-Login.yaml`. Make the change in the existing command snippet
so readers copying it will run the correct tutorial file.

Introduced refresh token rotation per OAuth2 Security BCP, ensuring consumed tokens are invalidated, and only newly issued tokens remain valid. Added tests to enforce single-use refresh token behavior. Updated tutorials and documentation for TLS best practices.

@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.

🧹 Nitpick comments (1)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/PasswordFlow.java (1)

54-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Broad catch (Exception e) swallows unrelated failures as invalid_client.

Catching Exception here will also swallow unexpected runtime errors (e.g. NPE, config issues) from getClientList(), mapping them to a misleading invalid_client response instead of surfacing the real cause.

🤖 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/oauth2/request/tokenrequest/PasswordFlow.java`
around lines 54 - 56, The broad catch in PasswordFlow’s token handling is
masking unrelated failures as invalid_client. Narrow the exception handling
around getClientList() and related client validation so only expected
authentication/client lookup failures are converted to that OAuth2 error, and
let unexpected runtime issues (for example null/config problems) propagate or be
handled separately. Use the PasswordFlow method and the getClientList() call
site to locate the fix.
🤖 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.

Nitpick comments:
In
`@core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/PasswordFlow.java`:
- Around line 54-56: The broad catch in PasswordFlow’s token handling is masking
unrelated failures as invalid_client. Narrow the exception handling around
getClientList() and related client validation so only expected
authentication/client lookup failures are converted to that OAuth2 error, and
let unexpected runtime issues (for example null/config problems) propagate or be
handled separately. Use the PasswordFlow method and the getClientList() call
site to locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3025b068-23a6-4c76-af79-4abf5c5d6336

📥 Commits

Reviewing files that changed from the base of the PR and between 1d40912 and fd66d65.

📒 Files selected for processing (22)
  • core/pom.xml
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/SessionFinder.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/CredentialsFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/PasswordFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/PasswordFlowClaimsTest.java
  • distribution/conf/log4j2.xml
  • distribution/pom.xml
  • distribution/src/test/java/com/predic8/membrane/tutorials/security/jwt/RequestingTokenTutorialTest.java
  • distribution/tutorials/security/41-JWT-Signing.yaml
  • distribution/tutorials/security/50-OAuth2-Basics.yaml
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/52-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml
  • distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml
  • docs/RELEASE_NOTES.md
  • docs/ROADMAP.md
  • pom.xml
  • war/pom.xml
💤 Files with no reviewable changes (1)
  • distribution/conf/log4j2.xml
✅ Files skipped from review due to trivial changes (3)
  • docs/ROADMAP.md
  • docs/RELEASE_NOTES.md
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/RefreshTokenFlow.java
🚧 Files skipped from review as they are similar to previous changes (11)
  • distribution/tutorials/security/50-OAuth2-Basics.yaml
  • distribution/tutorials/security/54a-OAuth2-Distributed-Issuer.yaml
  • distribution/tutorials/security/54b-OAuth2-Distributed-Validation.yaml
  • distribution/tutorials/security/53-OAuth2-Client-Token-Renewal.yaml
  • core/src/test/java/com/predic8/membrane/integration/withoutinternet/interceptor/oauth2/PasswordFlowClaimsTest.java
  • distribution/tutorials/security/51-OAuth2-Client-Credentials.yaml
  • distribution/tutorials/security/52-OAuth2-Password-Flow.yaml
  • distribution/tutorials/security/41-JWT-Signing.yaml
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/request/tokenrequest/CredentialsFlow.java

Comment thread core/src/main/java/com/predic8/membrane/core/security/JWTSecurityScheme.java Outdated
Comment thread distribution/tutorials/security/40-JWT-Requesting-Token.md Outdated
Comment thread docs/RELEASE_NOTES.md Outdated
Introduced a `scopesClaim` attribute to customize the claim used for scope validation in JWTSecurityScheme (e.g., "scp" or "scope"). Updated JwtAuthInterceptor to make use of this attribute. Refactored and expanded test cases for improved clarity and coverage, ensuring proper handling of scope claims.

@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)
core/src/main/java/com/predic8/membrane/core/security/JWTSecurityScheme.java (1)

29-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard scope splitting against leading/blank tokens.

" +" only trims trailing empty tokens, so a leading space (e.g. " read write") yields an empty-string scope, and a blank claim value produces {""}. Consider trimming and filtering empties. Also, the second if is logically an else if since a value can't be both String and List.

♻️ Proposed fix
-        var scopes = jwt.get(scopesClaim);
-        if (scopes instanceof String scopeString) {
-            this.scopes = new HashSet<>(Arrays.asList(scopeString.split(" +")));
-        }
-        if (scopes instanceof List<?> scopeList) {
-            this.scopes = new HashSet<>(scopeList.stream().map(Object::toString).toList());
-        }
+        var scopes = jwt.get(scopesClaim);
+        if (scopes instanceof String scopeString) {
+            this.scopes = Arrays.stream(scopeString.trim().split(" +"))
+                    .filter(s -> !s.isEmpty())
+                    .collect(Collectors.toCollection(HashSet::new));
+        } else if (scopes instanceof List<?> scopeList) {
+            this.scopes = new HashSet<>(scopeList.stream().map(Object::toString).toList());
+        }

(requires import java.util.stream.Collectors;)

🤖 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/security/JWTSecurityScheme.java`
around lines 29 - 37, In JWTSecurityScheme’s constructor, scope parsing can
create empty-string scopes for leading or blank values, and the two type checks
should not both run for the same claim. Update the String handling to trim the
claim and filter out empty tokens before building the HashSet, and change the
second scopes instanceof List<?> check to an else if so only one branch applies.
Use the existing scopes field and jwt/scopesClaim lookup to keep the fix
localized.
🤖 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/security/JWTSecurityScheme.java`:
- Around line 29-37: In JWTSecurityScheme’s constructor, scope parsing can
create empty-string scopes for leading or blank values, and the two type checks
should not both run for the same claim. Update the String handling to trim the
claim and filter out empty tokens before building the HashSet, and change the
second scopes instanceof List<?> check to an else if so only one branch applies.
Use the existing scopes field and jwt/scopesClaim lookup to keep the fix
localized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3c092261-cba5-4b0b-bb9c-7d685b6d6b0d

📥 Commits

Reviewing files that changed from the base of the PR and between 51a6746 and 7d43d94.

📒 Files selected for processing (11)
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/JwtAuthInterceptor.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/main/java/com/predic8/membrane/core/security/JWTSecurityScheme.java
  • core/src/test/java/com/predic8/membrane/core/security/JWTSecuritySchemeTest.java
  • distribution/release-notes/0.0.0.md
  • distribution/release-notes/5.1.17.md
  • distribution/release-notes/5.1.18.md
  • distribution/release-notes/5.1.19.md
  • distribution/tutorials/security/40-JWT-Requesting-Token.md
  • docs/RELEASE_NOTES.md
💤 Files with no reviewable changes (5)
  • distribution/release-notes/5.1.19.md
  • distribution/release-notes/5.1.17.md
  • distribution/release-notes/0.0.0.md
  • distribution/release-notes/5.1.18.md
  • docs/RELEASE_NOTES.md
✅ Files skipped from review due to trivial changes (1)
  • distribution/tutorials/security/40-JWT-Requesting-Token.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/tokengenerators/BearerJwtTokenGenerator.java
  • core/src/test/java/com/predic8/membrane/core/security/JWTSecuritySchemeTest.java
  • core/src/main/java/com/predic8/membrane/core/interceptor/jwt/Jwks.java

@predic8 predic8 requested a review from rrayst July 4, 2026 09:40
@rrayst rrayst merged commit f720446 into master Jul 4, 2026
5 checks passed
@rrayst rrayst deleted the tutorials-oauth2 branch July 4, 2026 13:05
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