Search code examples
javaspring-bootkeycloak

Spring Boot with React and Keycloak: redirect to Keycloak's login page fails due to CORS error (no Access-Control-Allow-Origin header set)


I'm working on the following setup:

  • Frontend: React on localhost:3000
  • Backend: Spring Boot on localhost:8080
  • OIDC endpoint: Keycloak 21+ as Docker container (tried different versions) on localhost:8090

As I'm a backend dev and have little to no experience with React, I wanted to let Spring handle all the authentication and authorization against Keycloak. Thus, I've configured:

spring:
  security:
  oauth2:
    client:
      registration:
        keycloak:
          clientId: <my id>
          clientSecret: <my secret>
          scope: openid
      provider:
        keycloak:
          issuerUri: <my issuer uri>
          user-name-attribute: preferred_username

and set the oauth2 config:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

    return http
            .csrf(Customizer.withDefaults())
            .cors(Customizer.withDefaults())
            .exceptionHandling(customizer -> customizer.accessDeniedHandler(customAccessDeniedHandler()))
            .authorizeHttpRequests((customizer) -> customizer.requestMatchers(
                        "/oauth2/login",
                        "/oauth2/logout/success",
                        "/oauth2/error",
                        
                ).permitAll()
                .anyRequest().authenticated()
            ).oauth2Login(configurer -> {
                configurer.userInfoEndpoint(customizer -> customizer.userAuthoritiesMapper(customGrantedAuthoritiesMapper()));
                configurer.loginPage("/oauth2/login");
                configurer.defaultSuccessUrl("/oauth2/success");
                configurer.failureUrl("/oauth2/error");
            });
}

When I access some protected endpoint (let's say /api/test) from React, Spring sends a redirect (per Location-header) to

http://localhost:8080/oauth2/login

which then redirects to

http://localhost:8080/oauth2/authorization/keycloak

which redirects to Keycloak on

http://localhost:8090/realms/my-realm/protocol/openid-connect/auth?response_type=code&client_id=my-client-id&scope=openid&state=my-state&redirect_uri=http://localhost:8086/login/oauth2/code/keycloak&nonce=my-nonce*

When calling it directly from the browser, it works fine and I see the login form, can login and access my protected resources afterwards with the right roles set into the principal. But as soon as the request's origin is my frontend webapp, the last redirect to Keycloak fails because of missing CORS header Access-Control-Allow-Origin.

Of course I followed the manual and set the valid resource urls as well as web origins inside Keycloak's client settings. But nothing worked, I tried to

  • set the frontend url without trailing slashes
  • set + and added http://localhost:3000/* to the valid redirect urls
  • set * as a test, even though it would open security loopholes

Nothing worked, I tried multiple solutions like those on StackOverflow, Discourse (1), Discourse (2) and multiple others, but to no avail. Unfortunately Keycloak changed a bit in the past (of course), so most articles are outdated. But finally I've stumbled upon this resource saying in point 7 that this CORS error indicates "wrong backend/API implementation or used flow".

So long story short: has anyone managed to get it running like described? Should I change my architecture to let React handle the authentication with Keycloak, sending JWT token to Spring instead?

Thank you very much for your opinions and insights.


Solution

  • Cross-origin issues happen only with XHR requests. But there is no need to perform authorization code flow (and RP-Initiated Logout) with the app internal HTTP client. If setting windows.location instead of using the internal HTTP client, redirections will be plain requests too and you won't have cross-origin issues (because requests are not cross-origin any-more).

    Two options when needing to log a user in:

    • set windows.location=http://localhost:8080/oauth2/login (but if you configure more than one registration with authorization_code, this is probably unacceptable).
    • use your app internal client to GET http://localhost:8080/oauth2/authorization/keycloak and configure your Spring app to answer in the 2xx range. The Javascript can then observe the response and follow the URI in the Location header. I do so in this Baeldung article I wrote for details.