-
-
Notifications
You must be signed in to change notification settings - Fork 221
feat: Add OAuth2 proxy support for authentication requests #1540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
joshuaNathaniel
wants to merge
6
commits into
kafbat:main
Choose a base branch
from
joshuaNathaniel:issues/196
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
5a314ac
feat: Add OAuth2 proxy support for authentication requests
joshuaNathaniel cf5e4f4
feat: Add OAuth2 proxy support using JVM system properties
joshuaNathaniel e97eae7
feat: Extend OAuth2 proxy support to all authentication endpoints
joshuaNathaniel b74c3ac
refactor: Make OAuth2 WebClient a qualified Spring bean
joshuaNathaniel 7964032
test: fix issue with netty proxy not allowing localhost as proxy on l…
joshuaNathaniel 7f49065
test: provide additional comments and guards for oauth test support
joshuaNathaniel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
api/src/main/java/io/kafbat/ui/config/auth/SimpleOAuthProxyConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package io.kafbat.ui.config.auth; | ||
|
|
||
| import lombok.Data; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.http.client.reactive.ReactorClientHttpConnector; | ||
| import org.springframework.web.reactive.function.client.WebClient; | ||
| import reactor.netty.http.client.HttpClient; | ||
|
|
||
| @Slf4j | ||
| @Configuration | ||
| @ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2") | ||
| @EnableConfigurationProperties(SimpleOAuthProxyConfig.ProxyProperties.class) | ||
| public class SimpleOAuthProxyConfig { | ||
|
|
||
| @Data | ||
| @ConfigurationProperties("auth.oauth2.proxy") | ||
| public static class ProxyProperties { | ||
| private boolean enabled = false; | ||
| private String host; | ||
| private Integer port; | ||
| } | ||
|
|
||
| @Bean(name = "oauth2WebClient") | ||
| @ConditionalOnProperty(value = "auth.oauth2.proxy.enabled", havingValue = "true") | ||
joshuaNathaniel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| public WebClient oauth2WebClient(ProxyProperties proxyProperties) { | ||
| HttpClient httpClient; | ||
|
|
||
| if (proxyProperties.getHost() != null && proxyProperties.getPort() != null) { | ||
| // Use explicit proxy configuration | ||
| log.info("OAuth2 configured with explicit proxy: {}:{}", | ||
| proxyProperties.getHost(), proxyProperties.getPort()); | ||
|
|
||
| httpClient = HttpClient.create() | ||
| .proxy(proxy -> proxy | ||
| .type(reactor.netty.transport.ProxyProvider.Proxy.HTTP) | ||
| .host(proxyProperties.getHost()) | ||
| .port(proxyProperties.getPort())); | ||
| } else { | ||
| // Use system proxy properties | ||
| log.info("OAuth2 configured to use system proxy properties"); | ||
| httpClient = HttpClient.create().proxyWithSystemProperties(); | ||
| } | ||
|
|
||
| return WebClient.builder() | ||
| .clientConnector(new ReactorClientHttpConnector(httpClient)) | ||
| .build(); | ||
| } | ||
| } | ||
151 changes: 151 additions & 0 deletions
151
api/src/test/java/io/kafbat/ui/config/auth/SimpleOAuthProxyConfigIntegrationTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| package io.kafbat.ui.config.auth; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import java.io.IOException; | ||
| import okhttp3.mockwebserver.MockResponse; | ||
| import okhttp3.mockwebserver.MockWebServer; | ||
| import okhttp3.mockwebserver.RecordedRequest; | ||
joshuaNathaniel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.web.reactive.function.client.WebClient; | ||
|
|
||
| class SimpleOAuthProxyConfigIntegrationTest { | ||
|
|
||
| private final MockWebServer oauth2Server = new MockWebServer(); | ||
|
|
||
| @BeforeEach | ||
| void startMockServer() throws IOException { | ||
| oauth2Server.start(); | ||
| } | ||
|
|
||
| @AfterEach | ||
| void stopMockServer() throws IOException { | ||
| oauth2Server.close(); | ||
| } | ||
|
|
||
| @Test | ||
| void testWebClientWithExplicitProxyCanMakeRequests() throws Exception { | ||
| // Create proxy config - in real usage, this would point to a proxy server | ||
| var proxyProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| proxyProps.setEnabled(true); | ||
| proxyProps.setHost("proxy.example.com"); | ||
| proxyProps.setPort(8080); | ||
|
|
||
| // Create WebClient with proxy configuration | ||
| var config = new SimpleOAuthProxyConfig(); | ||
| WebClient webClient = config.oauth2WebClient(proxyProps); | ||
| assertThat(webClient).isNotNull(); | ||
|
|
||
| // Mock OAuth2 server response | ||
| oauth2Server.enqueue(new MockResponse() | ||
| .setResponseCode(200) | ||
| .setBody("{\"access_token\": \"test-token\", \"token_type\": \"Bearer\"}") | ||
| .addHeader("Content-Type", "application/json")); | ||
|
|
||
| // Make a request to the mock server directly (not through proxy, since MockWebServer can't act as proxy) | ||
| // This tests that WebClient works and doesn't throw exceptions with proxy config | ||
| String response = webClient | ||
| .get() | ||
| .uri(oauth2Server.url("/oauth/token").toString()) | ||
| .retrieve() | ||
| .bodyToMono(String.class) | ||
| .onErrorReturn("{\"error\": \"proxy_not_available\"}") // Expected since proxy doesn't exist | ||
| .block(); | ||
|
|
||
| // In a real environment with a proxy, this would succeed | ||
| // Here we just verify the WebClient was created with proxy config | ||
| assertThat(response).contains("proxy_not_available"); | ||
| } | ||
joshuaNathaniel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @Test | ||
| void testWebClientWithSystemProxyProperties() { | ||
| // Set system properties | ||
| System.setProperty("https.proxyHost", "proxy.example.com"); | ||
| System.setProperty("https.proxyPort", "8080"); | ||
|
|
||
| try { | ||
| var proxyProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| proxyProps.setEnabled(true); | ||
| // Don't set host/port - should use system properties | ||
|
|
||
| var config = new SimpleOAuthProxyConfig(); | ||
| WebClient webClient = config.oauth2WebClient(proxyProps); | ||
|
|
||
| // Verify WebClient was created successfully with system proxy | ||
| assertThat(webClient).isNotNull(); | ||
| // Note: Can't easily test actual system proxy routing without a real proxy server | ||
| // But this verifies the configuration doesn't throw exceptions | ||
| } finally { | ||
| // Clean up system properties | ||
| System.clearProperty("https.proxyHost"); | ||
| System.clearProperty("https.proxyPort"); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| void testProxyDisabledDoesNotCreateWebClient() { | ||
| var proxyProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| proxyProps.setEnabled(false); | ||
|
|
||
| // When proxy is disabled, the bean should not be created | ||
| // This would normally be handled by Spring's @ConditionalOnProperty | ||
| // In unit test, we just verify the properties default | ||
| assertThat(proxyProps.isEnabled()).isFalse(); | ||
| } | ||
|
|
||
| @Test | ||
| void testWebClientCanMakeRequestsToOAuth2Provider() throws Exception { | ||
| // This test verifies the WebClient can communicate with an OAuth2 provider | ||
| // In production, the proxy would be between the WebClient and the OAuth2 provider | ||
|
|
||
| // Create a simple WebClient without proxy (simulating direct connection) | ||
| var config = new SimpleOAuthProxyConfig(); | ||
| var proxyProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| proxyProps.setEnabled(true); | ||
| proxyProps.setHost("localhost"); | ||
| proxyProps.setPort(oauth2Server.getPort()); | ||
|
|
||
| // Note: We can't actually test proxy behavior with MockWebServer | ||
| // as it doesn't implement proper proxy protocol (CONNECT method) | ||
| WebClient webClient = config.oauth2WebClient(proxyProps); | ||
|
|
||
| // Mock OAuth2 token endpoint | ||
| oauth2Server.enqueue(new MockResponse() | ||
| .setResponseCode(200) | ||
| .setBody("{\"access_token\": \"test-token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}") | ||
| .addHeader("Content-Type", "application/json")); | ||
|
|
||
| // Mock OAuth2 userinfo endpoint | ||
| oauth2Server.enqueue(new MockResponse() | ||
| .setResponseCode(200) | ||
| .setBody("{\"sub\": \"123456\", \"email\": \"[email protected]\"}") | ||
| .addHeader("Content-Type", "application/json")); | ||
|
|
||
| // Test token request (typical OAuth2 flow) | ||
| String tokenResponse = webClient.post() | ||
| .uri(oauth2Server.url("/oauth/token").toString()) | ||
| .bodyValue("grant_type=authorization_code&code=test-code") | ||
| .retrieve() | ||
| .bodyToMono(String.class) | ||
| .onErrorReturn("{\"error\": \"connection_failed\"}") | ||
| .block(); | ||
|
|
||
| // Test userinfo request (typical OAuth2 flow) | ||
| String userResponse = webClient.get() | ||
| .uri(oauth2Server.url("/oauth/userinfo").toString()) | ||
| .header("Authorization", "Bearer test-token") | ||
| .retrieve() | ||
| .bodyToMono(String.class) | ||
| .onErrorReturn("{\"error\": \"connection_failed\"}") | ||
| .block(); | ||
|
|
||
| // Verify WebClient was created and can attempt requests | ||
| // Actual proxy routing can only be verified with integration tests using real proxy | ||
| assertThat(webClient).isNotNull(); | ||
| // The requests will fail because localhost:port is not a real proxy server | ||
| assertThat(tokenResponse).contains("connection_failed"); | ||
| assertThat(userResponse).contains("connection_failed"); | ||
| } | ||
joshuaNathaniel marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
77 changes: 77 additions & 0 deletions
77
api/src/test/java/io/kafbat/ui/config/auth/SimpleOAuthProxyConfigTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package io.kafbat.ui.config.auth; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.web.reactive.function.client.WebClient; | ||
|
|
||
| class SimpleOAuthProxyConfigTest { | ||
|
|
||
| @Test | ||
| void proxyDisabledByDefault() { | ||
| var props = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
|
|
||
| assertThat(props.isEnabled()).isFalse(); | ||
| assertThat(props.getHost()).isNull(); | ||
| assertThat(props.getPort()).isNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void proxyPropertiesCanBeSet() { | ||
| var props = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
|
|
||
| props.setEnabled(true); | ||
| props.setHost("proxy.example.com"); | ||
| props.setPort(8080); | ||
|
|
||
| assertThat(props.isEnabled()).isTrue(); | ||
| assertThat(props.getHost()).isEqualTo("proxy.example.com"); | ||
| assertThat(props.getPort()).isEqualTo(8080); | ||
| } | ||
|
|
||
| @Test | ||
| void createsWebClientWithExplicitProxy() { | ||
| var config = new SimpleOAuthProxyConfig(); | ||
| var props = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| props.setEnabled(true); | ||
| props.setHost("proxy.example.com"); | ||
| props.setPort(8080); | ||
|
|
||
| WebClient webClient = config.oauth2WebClient(props); | ||
|
|
||
| assertThat(webClient).isNotNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void createsWebClientWithSystemProxy() { | ||
| var config = new SimpleOAuthProxyConfig(); | ||
| var props = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| props.setEnabled(true); | ||
| // No host/port - uses system properties | ||
|
|
||
| WebClient webClient = config.oauth2WebClient(props); | ||
|
|
||
| assertThat(webClient).isNotNull(); | ||
| } | ||
|
|
||
| @Test | ||
| void logsCorrectProxyTypeWhenCreatingWebClient() { | ||
| var config = new SimpleOAuthProxyConfig(); | ||
|
|
||
| // Test explicit proxy logging | ||
| var explicitProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| explicitProps.setEnabled(true); | ||
| explicitProps.setHost("proxy.example.com"); | ||
| explicitProps.setPort(3128); | ||
|
|
||
| WebClient explicitClient = config.oauth2WebClient(explicitProps); | ||
| assertThat(explicitClient).isNotNull(); | ||
|
|
||
| // Test system proxy logging | ||
| var systemProps = new SimpleOAuthProxyConfig.ProxyProperties(); | ||
| systemProps.setEnabled(true); | ||
|
|
||
| WebClient systemClient = config.oauth2WebClient(systemProps); | ||
| assertThat(systemClient).isNotNull(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.