Spring OAuth2 Keycloak Kubernetes internal/external access
Asked Answered
S

3

3

I have Keycloak (10.0.3) server configured inside a Kubernetes Cluster.

The keycloak server has to handle authentification for external user (using an external url) and also handle oauth2 token for Spring microservices communications.

Then web application spring services uses oidc providers :

spring:
  security:
    oauth2:
      client:
        provider:
          oidc:
            issuer-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm
            authorization-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/auth
            jwk-set-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/certs
            token-uri: http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm/protocol/openid-connect/token
            user-name-attribute: preferred_username

The external URL of keycloak is https://keycloak.localhost, managed by ingress redirection handled by Traefik v2

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: keycloak-https
  namespace: keycloak-cluster
  annotations:
    traefik.frontend.passHostHeader: "true"
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`keycloak.localhost`)
      kind: Rule
      services:
        - name: keycloak-cluster-http
          port: 80
  tls:
    options:
      name: mytlsoption
      namespace: traefik
    store:
      name: default

I can access Keycloak using https://keycloak.localhost, no problem, it works.

The problem is that when I try to access my web application, it will always redirect to 'http://keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/realms/myrealm', which is not resolved outside k8s.

If I change issuer-uri to http://keycloak.localhost then it doesn't work as keycloak.locahost is not resolved inside k8s.

I tried to set the KEYCLOAK_FRONTEND_URL to https://keycloak.localhost/auth, but no change.

Please, does someone has the same kind of settings and managed to make it working ?

Best regards

Scarfskin answered 27/9, 2020 at 21:46 Comment(1)
I think issuer-uri overwrites whatever you specify in authorization-uri and the other settings. Using issuer-uri Spring Security is auto-configured.Hickson
S
4

Managed to fix it using coredns and adding a rewrite rule... :

rewrite name keycloak.localhost keycloak-cluster-http.keycloak-cluster.svc.cluster.local

apiVersion: v1
data:
  Corefile: |
    .:53 {
        errors
        health
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        rewrite name keycloak.localhost keycloak-cluster-http.keycloak-cluster.svc.cluster.local
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
Scarfskin answered 30/9, 2020 at 8:25 Comment(0)
N
0

The authorization_uri needs to be understood by the browser since that URI is processed in the front channel. The rest of the URIs are processed in the back channel.

Because of that, the authorization_uri should use the front channel way of addressing the authorization server:

authorization_uri: https://keycloak.localhost/auth/realms/myrealm/protocol/openid-connect/auth

EDIT Based on Joe Grandja's input below, it appears that it's also necessary to not specify the issuer-uri property. The issuer-uri property is a shortcut for specifying the other URIs, and since you are specifying those, you don't need it anyway.

Nanon answered 28/9, 2020 at 17:52 Comment(6)
Thanks, a little better (I can get the login page, redirected by the frontend), but when login, I have a redirection to keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/… (which is not recognized by the browser).Scarfskin
If I clean the Frontend URL (in the Keycloak Realm Settings Page), then my Frontend application do not start saying : The Issuer "keycloak.localhost/auth/realms/myrealm" provided in the configuration metadata did not match the requested issuer "keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/…"Scarfskin
If I set the Frontend URL to keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth, the Frontend application starts, I get the login page, but after login, it try to redirect to keycloak-cluster-http.keycloak-cluster.svc.cluster.local/auth/….....Scarfskin
Since there is a Proxy in your environment setup, configuring the issuer-uri property can result in issues. Please take a look at the following 2 issues for a resolution around this. github.com/spring-projects/spring-security/issues/9019 github.com/spring-projects/spring-security/issues/…Mercuric
You're right, not needed by default, but in my case I use the issuer-uri to decode JWT token and extract info... The best and easiest solution I've found until now, is to add the rewrite rule on K8S side, then both hostname are redirected to keycloak. This solution works for also in Swarm Cluster and locally. If I have time, I'll try the workaround of Joe Grandja. Thanks for your help.Scarfskin
@Scarfskin how did you write the rewrite rule?Peony
R
0

Here A POC that helped me with the issue.
Similar configuration ,keycloak and spring gateway are in kubernetes
The external user uses keycloak external host with https protocol

https://external-https/auth/realms/myrealm/protocol/openid-connect/auth?...

The ingress break the https and moves it to http + change the host to internal-http

gateway uses internal-http to connect to keycloakon port 8080
In order for the issuer to be the same protocol as the external the configuration uses https in user-info-uri and authorization-uri but the rest are http make sure that the keycloak pod is open for https connection (8443)

authorization-uri: https://internal-http:8443/auth/realms/myrealm/protocol/openid-connect/auth
user-info-uri: https://internal-http:8443/auth/realms/myrealm/protocol/openid-connect/userinfo
issuer-uri: http://internal-http:8080/auth/realms/myrealm

To fix the host part of the issuer

In the gateway code I updated the following based on https://github.com/spring-projects/spring-security/issues/8882#user-content-oauth2-client

@SneakyThrows
private WebClient webClient() {
    SslContext sslContext = SslContextBuilder
            .forClient()
            .trustManager(InsecureTrustManagerFactory.INSTANCE)
            .build();
    HttpClient httpClient = HttpClient.create()
            .secure(t -> t.sslContext(sslContext))
            .wiretap(true)
            ;
    ReactorClientHttpConnector conn = new ReactorClientHttpConnector(httpClient);
    return WebClient.builder()
            .defaultHeader("HOST", "external-https")
            .clientConnector(conn)
            .build();
}
@Bean
WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient() {
    final WebClientReactiveAuthorizationCodeTokenResponseClient webClientReactiveAuthorizationCodeTokenResponseClient = new WebClientReactiveAuthorizationCodeTokenResponseClient();
    final WebClient webClient = webClient();
    webClientReactiveAuthorizationCodeTokenResponseClient.setWebClient(webClient);
    return webClientReactiveAuthorizationCodeTokenResponseClient;
}

@Bean
WebClientReactiveClientCredentialsTokenResponseClient webClientReactiveClientCredentialsTokenResponseClient() {
    final WebClientReactiveClientCredentialsTokenResponseClient webClientReactiveClientCredentialsTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
    final WebClient webClient = webClient();
    webClientReactiveClientCredentialsTokenResponseClient.setWebClient(webClient);
    return webClientReactiveClientCredentialsTokenResponseClient;
}

@Bean
WebClientReactiveRefreshTokenTokenResponseClient webClientReactiveRefreshTokenTokenResponseClient() {
    final WebClientReactiveRefreshTokenTokenResponseClient webClientReactiveRefreshTokenTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient();
    final WebClient webClient = webClient();
    webClientReactiveRefreshTokenTokenResponseClient.setWebClient(webClient);
    return webClientReactiveRefreshTokenTokenResponseClient;
}

@Bean
WebClientReactivePasswordTokenResponseClient webClientReactivePasswordTokenResponseClient() {
    final var client = new WebClientReactivePasswordTokenResponseClient();
    final WebClient webClient = webClient();
    client.setWebClient(webClient);
    return client;
}


@Bean
DefaultReactiveOAuth2UserService reactiveOAuth2UserService() {
    final DefaultReactiveOAuth2UserService userService = new DefaultReactiveOAuth2UserService();
    final WebClient webClient = webClient();
    userService.setWebClient(webClient);
    return userService;
}
  • Disabled the certificate validation - the connection is only between keycloak and gateway , both are in the kubernetes and otherwise would have used http connection, if not for this issue
  • The host part tells the keyclock what is the host to use for the issuer

Another issue encountered is that the location return when redirecting to authentication contains the internal url and not the external which the outside world doesn't know of
For that ,update the location that returns from the gateway

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
...
oauth2Login(oAuth2LoginSpec -> oAuth2LoginSpec
     ...
     .addFilterAfter(new LoginLocationFilter("external-https"), SecurityWebFiltersOrder.LAST)
     ...

 public class LoginLocationFilter implements WebFilter {
     private final String externalUrl;

    public LoginLocationFilter(String externalUrl) {
        this.externalUrl = externalUrl;

    }
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        //before commit ,otherwise the headers will be read only
        exchange.getResponse().beforeCommit(() -> {
            fixLocation(exchange);
            return Mono.empty();
        });
        return chain.filter(exchange);
    } 
...
Ruggiero answered 23/10, 2022 at 5:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.