provectus / kafka-ui

Open-Source Web UI for Apache Kafka Management
Apache License 2.0
9.75k stars 1.18k forks source link

BE: Respect proxy settings for OAuth requests #4114

Open poom-kitti opened 1 year ago

poom-kitti commented 1 year ago

Issue submitter TODO list

Describe the bug (actual behavior)

To give some context, our Kafka UI is deployed in a server inside a virtual private network. This mean:

When Kafka UI is deployed in the server, it does not respect using proxy declared in any of the following Java system properties when performing connection to Okta to perform authentication:

Once a client tried to connect to Kafka UI, they are directed to authenticate to Okta correctly. However, after the client passed the code which they received from Okta back to Kafka UI triggering Kafka UI to perform POST request to Okta's token URI, it fails due to java.net.NoRouteToHostException indicating that the proxy is not in-use.

From some exploration, I believe that Spring security is using the DefaultWebClient to perform authentication and this WebClient does not respect the system properties on using proxy.

Expected behavior

When specified the system property like -Dhttps.proxyHost=my.proxy.host -Dhttps.proxyPort=3218 -Dhttps.nonProxyHosts=XXXX.local|10.*, the authentication should be using proxy to make connection to Okta.

Your installation details

App version: 0.7.1 (as of commit b32ab0143679bd3224f097a9de0eefad4e60f8d6)

Application YAML: I deliberately did not add any configuration regarding connection to Kafka as it is unnecessary to show behavior of Okta authentication. In addition, this way, it make showing debug log clearer as we will only get debug log regarding authentication.

auth:
  type: OAUTH2
  oauth2:
    client:
      okta:
        clientId: <client-id>
        clientSecret: <client-secret>
        scope: [ 'openid', 'profile', 'email', 'groups' ]
        client-name: Okta
        provider: okta
        redirect-uri: "{baseUrl}/login/oauth2/code/okta"
        authorization-grant-type: authorization_code
        issuer-uri: https://<okta-endpoint>
        authorization-uri: https://<okta-endpoint>/oauth2/v1/authorize
        token-uri: https://<okta-endpoint>/oauth2/v1/token
        user-info-uri: https://<okta-endpoint>/oauth2/v1/userinfo
        jwk-set-uri: https://<okta-endpoint>/oauth2/v1/keys
        user-name-attribute: email
        custom-params:
          type: oauth
          roles-field: groups
server:
  port: 8080

Steps to reproduce

  1. Add authentication with Okta configurations.
  2. Build the Docker image.
  3. Create a Docker container inside a server that cannot access the internet except through proxy server.
  4. Start Kafka UI with Java system properties related to proxy:
    • http.proxyHost
    • http.proxyPort
    • http.nonProxyHosts
    • https.proxyHost
    • https.proxyPort
    • https.nonProxyHosts
  5. Try to access Kafka UI and perform Okta authentication.

Screenshots

From the network tab in developer tool, we can see that Okta returns some code to user and this is passed to Kafka UI; however the get request to <kafka ui endpoint>/login/oauth2/code/okta?code=<some code> failed.

error_pixelate

Logs

From logs, I see the following error:

2023-08-12 22:48:08,259 ERROR [reactor-http-epoll-3] o.s.b.a.w.r.e.AbstractErrorWebExceptionHandler: [8200782c-3]  500 Server Error for HTTP GET "/login/oauth2/code/okta?code=pV9h3TwnnHan7NjkEYWZR6oPKMcBU8WTrH3m4tLMX40&state=VHDFtviuySJvVcmLMwurglU1u81s1mw1Bw32_BlUD4Y%3D"
org.springframework.web.reactive.function.client.WebClientRequestException: null: dev-61615254.okta.com/99.83.233.105:443
    at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:136)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to POST https://dev-61615254.okta.com/oauth2/v1/token [DefaultWebClient]
    *__checkpoint ⇢ OAuth2LoginAuthenticationWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ OAuth2AuthorizationRequestRedirectWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ ReactorContextWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.web.filter.reactive.ServerHttpObservationFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ HTTP GET "/login/oauth2/code/okta?code=pV9h3TwnnHan7NjkEYWZR6oPKMcBU8WTrH3m4tLMX40&state=VHDFtviuySJvVcmLMwurglU1u81s1mw1Bw32_BlUD4Y%3D" [ExceptionHandlingWebHandler]
Original Stack Trace:
        at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.lambda$wrapException$9(ExchangeFunctions.java:136)
        xxx
Caused by: io.netty.channel.AbstractChannel$AnnotatedNoRouteToHostException: null: dev-61615254.okta.com/99.83.233.105:443
Caused by: java.net.NoRouteToHostException: null
xxx

When setting the environment variable LOGGING_LEVEL_ROOT=debug to show debug log, I see the following logs that signify that Kafka UI is trying to connect to the Okta endpoint directly. This should not be the case because it should use proxy instead.

2023-08-12 22:51:21,497 DEBUG [reactor-http-epoll-3] o.s.w.r.f.c.ExchangeFunctions: [62de54f3] HTTP POST https://dev-61615254.okta.com/oauth2/v1/token
2023-08-12 22:51:21,684 DEBUG [reactor-http-epoll-3] i.n.r.d.DnsNameResolver: [id: 0xfd2c1ad8] RECEIVED: UDP [43221: /127.0.0.11:53], DatagramDnsResponse(from: /127.0.0.11:53, id: 43221, QUERY(0), NoError(0), RD RA)
    DefaultDnsQuestion(dev-61615254.okta.com. IN A)
    DefaultDnsRawRecord(dev-61615254.okta.com. 270 IN CNAME 25B)
    DefaultDnsRawRecord(ok12-crtrs.tng.okta.com. 30 IN CNAME 30B)
    DefaultDnsRawRecord(ok12-crtrs.oktaedge.okta.com. 270 IN CNAME 44B)
    DefaultDnsRawRecord(a1c0075a909445e0e.awsglobalaccelerator.com. 75 IN A 4B)
    DefaultDnsRawRecord(a1c0075a909445e0e.awsglobalaccelerator.com. 75 IN A 4B)
    DefaultDnsRawRecord(OPT flags:0 udp:4000 0B)
2023-08-12 22:51:21,688 DEBUG [reactor-http-epoll-3] r.n.t.TransportConnector: [5de47a0b] Connecting to [dev-61615254.okta.com/75.2.37.199:443].
2023-08-12 22:51:21,690 DEBUG [reactor-http-epoll-3] r.n.t.TransportConnector: [5de47a0b] Connect attempt to [dev-61615254.okta.com/75.2.37.199:443] failed.       

Additional context

No response

github-actions[bot] commented 1 year ago

Hello there poom-kitti! 👋

Thank you and congratulations 🎉 for opening your very first issue in this project! 💖

In case you want to claim this issue, please comment down below! We will try to get back to you as soon as we can. 👀

poom-kitti commented 1 year ago

I did get the proxy to work by implementing OidcAuthorizationCodeReactiveAuthenticationManager (OAuth2 equivalent is OAuth2LoginReactiveAuthenticationManager) and set to use this manager for login.

The manager seems to interact with Okta for 3 things and the following classes are used for those interactions:

My Solution

I get the proxy system properties to work by making following changes in OAuthSecurityConfig:

@Configuration
@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
@EnableConfigurationProperties(OAuthProperties.class)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@RequiredArgsConstructor
@Log4j2
public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {

  private final OAuthProperties properties;

  // 1. Create WebClient that respects proxy settings
  private WebClient webClient = new WebClientConfigurator().build();

  @Bean
  public SecurityWebFilterChain configure(
      ServerHttpSecurity http,
      OAuthLogoutSuccessHandler logoutHandler,
      CustomOidcAuthenticationManagerBuilder oidcAuthenticationManagerBuilder
  ) {
    log.info("Configuring OAUTH2 authentication.");

    return http.authorizeExchange(spec -> spec
            .pathMatchers(AUTH_WHITELIST)
            .permitAll()
            .anyExchange()
            .authenticated()
        )
        .oauth2Login(spec -> spec.authenticationManager(oidcAuthenticationManagerBuilder.build())) // 2. Use custom authentication manager
        .logout(spec -> spec.logoutSuccessHandler(logoutHandler))
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .build();
  }

  @Bean
  public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(AccessControlService acs) {
    // 3. Use WebClient that respects proxy settings to get user-info
    final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
    delegate.setOauth2UserService(customOauth2UserService(acs));
    ...
  }

  @Bean
  public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {
    // 3. Use WebClient that respects proxy settings to get user-info
    final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
    delegate.setWebClient(webClient);
    ...
  }

This is the code for CustomOidcAuthenticationManagerBuilder that I created (in a new file):

@Component
@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
public class CustomOidcAuthenticationManagerBuilder {
  private WebClient webClient = new WebClientConfigurator().build();
  // Reuse customOidcUserService bean from `OAuthSecurityConfig`
  private ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;
  @Value("${auth.oauth2.client.okta.jwk-set-uri}")
  private String jwkUri;

  public CustomOidcAuthenticationManagerBuilder(ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
    this.oidcUserService = oidcUserService;
  }

  public OidcAuthorizationCodeReactiveAuthenticationManager build() {
    // Use WebClient that respects proxy settings to get token
    WebClientReactiveAuthorizationCodeTokenResponseClient client =
        new WebClientReactiveAuthorizationCodeTokenResponseClient();
    client.setWebClient(webClient);

    // Use WebClient that respects proxy settings to get JWT
    ReactiveJwtDecoderFactory<ClientRegistration> idTokenDecoderFactory =
        (clientRegistration) -> NimbusReactiveJwtDecoder.withJwkSetUri(jwkUri)
            .webClient(webClient)
            .build();

    OidcAuthorizationCodeReactiveAuthenticationManager manager =
        new OidcAuthorizationCodeReactiveAuthenticationManager(client, oidcUserService);
    manager.setJwtDecoderFactory(idTokenDecoderFactory);

    return manager;
  }
}

Please check my commit which should make this clearer: https://github.com/poom-kitti/kafka-ui/commit/3b88135168d99618bfa40da37885b996bb6d96a8

Result

After applying the changes and rebuild Kafka UI along with deploying (using same Java system properties related to proxy), the authentication now works. I can confirm that proxy is used because from debug logs, instead of making connection to Okta directly, it is connecting to proxy instead.

[30m2023-08-12 23:15:49,863 DEBUG [reactor-http-epoll-4] o.s.w.r.f.c.ExchangeFunctions: [52dec0ff] HTTP POST https://dev-61615254.okta.com/oauth2/v1/token
2023-08-12 23:15:49,865 DEBUG [reactor-http-epoll-3] r.n.r.PooledConnectionProvider: [d894b024, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128] Channel acquired, now: 1 active connections, 1 inactive connections and 0 pending acquire requests.
2023-08-12 23:15:49,865 DEBUG [reactor-http-epoll-3] r.n.h.c.HttpClientConnect: [d894b024-2, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128] Handler is being applied: {uri=https://dev-61615254.okta.com/oauth2/v1/token, method=POST}
2023-08-12 23:15:49,865 DEBUG [reactor-http-epoll-3] r.n.r.DefaultPooledConnectionProvider: [d894b024-2, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128] onStateChange(POST{uri=/oauth2/v1/token, connection=PooledConnection{channel=[id: 0xd894b024, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128]}}, [request_prepared])
2023-08-12 23:15:49,866 DEBUG [reactor-http-epoll-3] o.s.h.c.FormHttpMessageWriter: [52dec0ff] Writing form fields [grant_type, code, redirect_uri] (content masked)
2023-08-12 23:15:49,867 DEBUG [reactor-http-epoll-3] r.n.r.DefaultPooledConnectionProvider: [d894b024-2, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128] onStateChange(POST{uri=/oauth2/v1/token, connection=PooledConnection{channel=[id: 0xd894b024, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128]}}, [request_sent])
2023-08-12 23:15:50,128 DEBUG [reactor-http-epoll-3] r.n.h.c.HttpClientOperations: [d894b024-2, L:/172.19.0.4:36860 - R:my.proxy.host/10.118.87.223:3128] Received response (auto-read:false) : RESPONSE(decodeResult: success, version: HTTP/1.1)

Caveats

The drawback of my current implementations are:

  1. It should only work with Oauth2 authentication that uses Open ID Connect protocol (not certain is this working for all Oauth2 authentication)
  2. For NimbusReactiveJwtDecoder, it is using a static jwk-set-uri to point to Okta configuration which would not be generic if the provider is not Okta

Since I lack experience with Spring, I do not wish to claim this issue, but hope my investigation help in some way.

Haarolean commented 1 year ago

@poom-kitti Hi and thank you very much for the analysis, saved me some time in research.

Even if we implement the changes in other places, we won't be able to decode JWT token without these changes. In the case of Okta / OIDC, we'd use the underlying OidcAuthorizationCodeReactiveAuthenticationManager, which, in turn, does use ReactiveOidcIdTokenDecoderFactory, which doesn't allow customizing the webclient used for NimbusReactiveJwtDecoder. This behavior should be fixed within this spring-security issue: https://github.com/spring-projects/spring-security/issues/13274. There's already a PR but getting one merged and released would take some time.

Related: https://github.com/spring-projects/spring-security/issues/8882 Until that one is done, we'd have to customize the webclient manually for the following beans:

and probably some others which we need to determine on a per-use-case basis.

I suggest we put this on hold for some time to see if https://github.com/spring-projects/spring-security/issues/13274 does get any traction to see if we can avoid copy-pasting the whole factory bean.

joachimBurket commented 9 months ago

Hi,

Is there a workaround not involving editing the java code for this? Or do we have to wait for it to be solved?

It seems the issue https://github.com/spring-projects/spring-security/issues/13274 is now closed. What are the next steps?

1300371 commented 7 months ago

Some news about this fix ?

Haarolean commented 7 months ago

@1300371 this repo is not maintained: source. Please take a look at the mentioned issue right above your comment as well.

seb2020 commented 3 months ago

Some news about this fix ?

Haarolean commented 3 months ago

@seb2020 see the comment above yours