Skip to content

Conversation

rrayst
Copy link
Member

@rrayst rrayst commented Aug 26, 2025

Summary by CodeRabbit

  • New Features
    • Added configurable OAuth2 client authentication methods (header or body) and support for client assertions (JWT) when requesting tokens.
  • Bug Fixes
    • Users are logged out if an error occurs during a second OAuth2 flow, improving session safety.
  • Tests
    • Expanded test coverage and mock server behavior to validate client credential handling and second-flow error handling.

rrayst and others added 2 commits August 26, 2025 10:26
* assert that OAuth2 error during second flow logs user out
* added configuration option to copy client_id parameter to token endpoint's request body
* renamed clientIdInBody='true' to clientAuthorization='client_secret_post' to align config with OIDC terminology
* refactoring to use ParameterNames.* constants
@rrayst rrayst requested a review from predic8 August 26, 2025 08:27
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Walkthrough

Refactors OAuth2 token request construction to use a builder (OAuth2TokenBody), introduces ClientAuthorization enum and configurable client auth behavior in AuthorizationService (choose Basic header vs POST body), updates tests and mock server to assert body-based client credentials and adds a new logout-on-error test.

Changes

Cohort / File(s) Summary
OAuth2 token body builder
core/src/main/java/.../oauth2/OAuth2TokenBody.java
Adds private fields clientId, clientSecret, clientAssertion, clientAssertionType; makes constructor private; adds builder methods clientId(...), clientSecret(...), clientAssertion(type, assertion); build() appends new client-related params and uses appendParam for scope.
Authorization service refactor
core/src/main/java/.../oauth2/authorizationservice/AuthorizationService.java
Changes applyAuth to accept OAuth2TokenBody; adds ClientAuthorization config with getter/setter; branches sending credentials via Basic auth header (client_secret_basic) or in POST body (client_secret_post) or only client_id when secret absent; integrates JWT client assertions through the builder.
Client auth enum
core/src/main/java/.../oauth2/authorizationservice/ClientAuthorization.java
Adds public enum ClientAuthorization with client_secret_basic and client_secret_post.
B2C test configuration
core/src/test/java/.../client/b2c/B2CMembrane.java
Configures MembraneAuthorizationService to use client_secret_post.
Mock authorization server
core/src/test/java/.../client/b2c/MockAuthorizationServer.java
Replaces hardcoded param names with ParamNames constants; adjusts parsing strategy imports; asserts client_id/client_secret from body; removes Basic header validation; updates scope/param key usages.
B2C integration tests
core/src/test/java/.../client/b2c/OAuth2ResourceB2CTest.java
Adds errorDuringSecondOAuth2FlowLogsUserOut() test verifying logout when second OAuth2 flow returns an error.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant AS as AuthorizationService
  participant TB as OAuth2TokenBody
  participant TE as TokenEndpoint

  Client->>AS: request token (flowContext)
  AS->>TB: compose grant params (grant_type, code/refresh_token, redirect_uri, scope...)
  alt client_assertion provided
    AS->>TB: clientAssertion(type, assertion)
  end

  alt ClientAuthorization = client_secret_basic
    AS->>AS: create Authorization: Basic(clientId:clientSecret)
    AS->>TE: POST /token with Authorization header and TB.build()
  else ClientAuthorization = client_secret_post
    AS->>TB: clientId(id).clientSecret(secret)
    AS->>TE: POST /token with TB.build() (credentials in body)
  else no clientSecret
    AS->>TB: clientId(id)
    AS->>TE: POST /token with TB.build() (no secret)
  end

  TE-->>AS: token response / error
  AS-->>Client: propagate result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Prio-1

Suggested reviewers

  • predic8
  • t-burch

Poem

A rabbit builds tokens in tidy rows,
Hops between Basic and post where it goes.
Assertion tucked, secret kept or shared,
If flows fail, I twitch—session pared.
Carrot commit, we ship with a nose twitch. 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5a19a2b and 8471da8.

📒 Files selected for processing (1)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Automated tests
  • GitHub Check: Analyze (java)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch oauth2_improvements

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (9)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/ClientAuthorization.java (1)

3-11: Enum values intentionally mirror spec tokens; add rationale and plan for future methods

Using lowercase enum constants matches OIDC/OAuth2 token_endpoint_auth_method values and is convenient when serialized. Add a brief note that these identifiers are intentionally aligned with the spec to justify deviating from Java’s usual UPPER_SNAKE_CASE, and consider outlining whether support for client_secret_jwt/private_key_jwt is planned (the rest of the PR already introduces client assertion fields).

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java (3)

56-70: Guard against contradictory client auth inputs

Right now callers can set both clientSecret and clientAssertion(_Type). That combination is invalid for a single request. Add a simple validation (either here or in build()) to fail fast if both methods are configured.

Apply this diff inside build() (see lines 72-84) to reject invalid combinations:

+        if ((clientAssertionType != null || clientAssertion != null) && clientSecret != null) {
+            throw new IllegalStateException("Specify either client_secret (client_secret_basic/post) or client_assertion(_type) (private_key_jwt/client_secret_jwt), not both.");
+        }
+        if ((clientAssertionType == null) != (clientAssertion == null)) {
+            throw new IllegalStateException("client_assertion_type and client_assertion must be provided together.");
+        }

72-84: Normalize form-encoding; prefer URLParamUtil.ParamBuilder over manual concatenation

Manual concatenation partially encodes only scope, leaving other values (e.g., redirect_uri) unencoded. For correctness and maintainability, build the body with URLParamUtil.ParamBuilder, which percent-encodes consistently. This also lets you drop appendParam helpers.

Apply this diff:

-    public String build() {
-        StringBuilder r = new StringBuilder("grant_type=" + grantType);
-        appendParam(r, "refresh_token", refreshToken);
-        appendParam(r, "code", code);
-        appendParam(r, "redirect_uri", redirectUri);
-        appendParam(r, "scope", scope, e -> encode(e, UTF_8));
-        appendParam(r, "code_verifier", codeVerifier);
-        appendParam(r, "client_id", clientId);
-        appendParam(r, "client_secret", clientSecret);
-        appendParam(r, "client_assertion_type", clientAssertionType);
-        appendParam(r, "client_assertion", clientAssertion);
-        return r.toString();
-    }
+    public String build() {
+        if (grantType == null)
+            throw new IllegalStateException("grant_type must be set.");
+
+        // disallow contradictory/malformed client auth (see separate comment)
+        if ((clientAssertionType != null || clientAssertion != null) && clientSecret != null)
+            throw new IllegalStateException("Specify either client_secret or client_assertion(_type), not both.");
+        if ((clientAssertionType == null) != (clientAssertion == null))
+            throw new IllegalStateException("client_assertion_type and client_assertion must be provided together.");
+
+        URLParamUtil.ParamBuilder pb = new URLParamUtil.ParamBuilder();
+        pb.add("grant_type", grantType);
+        if (refreshToken != null) pb.add("refresh_token", refreshToken);
+        if (code != null) pb.add("code", code);
+        if (redirectUri != null) pb.add("redirect_uri", redirectUri);
+        if (scope != null) pb.add("scope", scope);
+        if (codeVerifier != null) pb.add("code_verifier", codeVerifier);
+        if (clientId != null) pb.add("client_id", clientId);
+        if (clientSecret != null) pb.add("client_secret", clientSecret);
+        if (clientAssertionType != null) pb.add("client_assertion_type", clientAssertionType);
+        if (clientAssertion != null) pb.add("client_assertion", clientAssertion);
+        return pb.build();
+    }

Note: You’ll need to import URLParamUtil (and can optionally switch to using ParamNames constants via a static import for consistency).


86-94: Remove now-redundant helpers after switching to ParamBuilder

If you adopt the ParamBuilder approach above, these appendParam helpers become unused and can be deleted.

Apply this diff:

-    private void appendParam(StringBuilder sb, String paramName, String paramValue) {
-        appendParam(sb, paramName, paramValue, e -> e);
-    }
-
-    private void appendParam(StringBuilder sb, String paramName, String paramValue, Function<String, String> encoder) {
-        if (paramValue == null)
-            return;
-        sb.append("&").append(paramName).append("=").append(encoder.apply(paramValue));
-    }
+    // removed: manual param appending; build() now uses URLParamUtil.ParamBuilder
core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/B2CMembrane.java (1)

155-164: Consider parameterizing auth method to keep coverage for client_secret_basic too

You removed Basic header assertions on the server side. To prevent regressions, consider parameterizing this test fixture to run once with client_secret_basic and once with client_secret_post. That makes both paths observable in CI without duplicating scenarios.

I can provide a JUnit @ParameterizedTest variant or a small test matrix utility if you want to run the same suite against both auth modes.

core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/MockAuthorizationServer.java (1)

165-174: Make client auth assertions robust to both Basic and POST

The current assertions require client_id/client_secret in the body (client_secret_post). If future tests switch to client_secret_basic, this will fail silently for incorrect header handling. Prefer validating either Basic header or POST body depending on what’s present.

Apply this diff to support both methods:

-        Map<String, String> params = URLParamUtil.getParams(new URIFactory(), exc, ERROR);
-        assertEquals(tc.clientId, params.get(CLIENT_ID));
-        assertEquals(tc.clientSecret, params.get(CLIENT_SECRET));
+        Map<String, String> params = URLParamUtil.getParams(new URIFactory(), exc, ERROR);
+        String authz = exc.getRequest().getHeader().getFirstValue("Authorization");
+        if (authz != null && authz.startsWith("Basic ")) {
+            String decoded = new String(Base64.getDecoder().decode(authz.substring("Basic ".length())), UTF_8);
+            String[] parts = decoded.split(":", 2);
+            assertEquals(tc.clientId, parts[0], "client_id (basic)");
+            assertEquals(tc.clientSecret, parts[1], "client_secret (basic)");
+        } else {
+            assertEquals(tc.clientId, params.get(CLIENT_ID), "client_id (post)");
+            assertEquals(tc.clientSecret, params.get(CLIENT_SECRET), "client_secret (post)");
+        }
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java (3)

59-62: Tighten imports: add ISO_8859_1 and drop unused ones

encode() and UTF_8 are not used here; ISO_8859_1 is needed for the Basic header fix.

Apply this diff:

-import static java.net.URLEncoder.encode;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static org.apache.commons.codec.binary.Base64.encodeBase64;

48-48: Remove unused import

URLEncoder is not used in this class.

Apply this diff:

-import java.net.URLEncoder;

233-247: Clarify precedence between clientAuthorization and useJWTForClientAuth

Config can currently set both useJWTForClientAuth=true and clientAuthorization=client_secret_*. The applyAuth refactor (above) makes JWT take precedence at runtime, but consider validating configuration during init() to fail fast or at least warn when both are configured.

Option (outside this hunk):

private void validateClientAuthConfig() {
    if (isUseJWTForClientAuth() && getClientSecret() != null) {
        log.warn("useJWTForClientAuth=true and clientSecret provided; JWT auth will be used and client_secret ignored.");
    }
}

Call from init() after properties are set.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c78b6a1 and 5a19a2b.

📒 Files selected for processing (6)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java (2 hunks)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java (7 hunks)
  • core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/ClientAuthorization.java (1 hunks)
  • core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/B2CMembrane.java (1 hunks)
  • core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/MockAuthorizationServer.java (4 hunks)
  • core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/OAuth2ResourceB2CTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/MockAuthorizationServer.java (2)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/ParamNames.java (1)
  • ParamNames (16-48)
core/src/main/java/com/predic8/membrane/core/util/URLParamUtil.java (1)
  • URLParamUtil (31-159)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java (2)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java (1)
  • OAuth2TokenBody (22-100)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2client/rf/token/JWSSigner.java (1)
  • JWSSigner (26-53)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (java)
🔇 Additional comments (9)
core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/OAuth2TokenBody.java (2)

29-33: Good: client credentials and assertion fields added cohesively

The additional fields (clientId, clientSecret, clientAssertion, clientAssertionType) align with token_endpoint authentication options and the builder pattern used elsewhere.


34-34: Good: private ctor enforces builder usage

Making the constructor private nudges callers to the static builders and fluent setters.

core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/B2CMembrane.java (1)

155-164: Explicitly exercising client_secret_post in tests is good

Setting client_secret_post here ensures tests validate the POSTed credentials path end-to-end.

core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/OAuth2ResourceB2CTest.java (1)

509-521: Nice addition: verifies logout on error during the second flow

This complements multipleUserFlowsWithErrorTest by explicitly asserting the session logout state via /is-logged-in.

core/src/test/java/com/predic8/membrane/core/interceptor/oauth2/client/b2c/MockAuthorizationServer.java (3)

44-46: Good: replace magic strings with ParamNames constants and static ERROR

The static imports improve consistency and reduce typos across tests.


131-158: OK: consistent parameter parsing in one place

Using URLParamUtil.getParams(..., ERROR) uniformly keeps parsing strict. No issues here.


192-193: Good: use SCOPE constant

Keeps parameter handling aligned with the rest of the code and avoids string drift.

core/src/main/java/com/predic8/membrane/core/interceptor/oauth2/authorizationservice/AuthorizationService.java (2)

340-347: Good use of builder for authorization_code flow

Passing redirectUri via the builder and delegating auth materialization to applyAuth is the right direction.


82-82: Sensible default for ClientAuthorization

Defaulting to client_secret_basic matches common provider defaults and keeps backward compatibility.

@rrayst
Copy link
Member Author

rrayst commented Aug 26, 2025

/ok-to-test

@christiangoerdes christiangoerdes merged commit 0f3753f into master Sep 9, 2025
1 of 3 checks passed
@christiangoerdes christiangoerdes deleted the oauth2_improvements branch September 9, 2025 10:48
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.

2 participants