Preface

  • I asked this question on Stack Overflow but I would like to encourage enhancing the Spring documentation for such a use case.
  • I also read this comment in #6638 as it looks quite similar to what I'm trying to achieve but I'm not sure if this is really the case.

I'm trying to setup a proof-of-concept like in the following diagram:

  • SPA is a VueJS application.
  • Gateway and API service are Spring Boot applications (version 2.6.3).
  • OAuth2 provider is Keycloak.

Observations

Scenario 1 (browser without SPA)

When I use the browser without the SPA, then the following workflow works as intended:

  1. Browser hits http://localhost:8555/api/messages.
  2. Gateway redirects to Keycloak login form.
  3. After successful authentication, the browser can access http://localhost:8555/api/messages and a SESSION cookie is created.
  4. The response from /api/messages is shown in the browser.

Scenario 2 (browser with SPA)

This is the scenario I'd like to actually realize. The observed workflow here is:

  1. User calls SPA on http://localhost:8093.
  2. SPA fires a GET request to http://localhost:8555/api/messages.
  3. The user is not redirected to the authentication provider.

Instead, there a several messages in the browser console indicating CORS issues.

Console

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=my-client&state=vpgnHaxh-5nb_LxksYL1a-PRU0tzuHR5itVvw1ovD-E%3D&redirect_uri=http://localhost:8555/login/oauth2/code/keycloak. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

Network Traffic

As far as I can see, the following is happening:

  1. GET to http://localhost:8555/api/messages is answered with a 302 and location /oauth2/authorization/keycloak.
  2. Another GET to http://localhost:8555/oauth2/authorization/keycloak is answered with a 302 and Location: http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=my-client&state=vpgnHaxh-5nb_LxksYL1a-PRU0tzuHR5itVvw1ovD-E%3D&redirect_uri=http://localhost:8555/login/oauth2/code/keycloak
  3. GET to http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=my-client&state=vpgnHaxh-5nb_LxksYL1a-PRU0tzuHR5itVvw1ovD-E%3D&redirect_uri=http%3A%2F%2Flocalhost%3A8555%2Flogin%2Foauth2%2Fcode%2Fkeycloak is answered with 200.

But all of them have CORS issues.

Spring Boot Implementation

Gateway

application.yml

server:
  port: 8555

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:8080/auth/realms/demo
            user-name-attribute: preferred_username
        registration:
          keycloak:
            provider: keycloak
            client-id: my-client
            client-secret: my-secret
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
  cloud:
    gateway:
      default-filters:
        - TokenRelay
        - RemoveRequestHeader=Cookie
      routes:
        - id: api-service
          uri: http://localhost:8092
          predicates:
            - Path=/api/**

SecurityConfiguration

@Configuration
@EnableWebFluxSecurity
public class SecurityConfiguration {

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setExposedHeaders(Arrays.asList("*"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
        httpSecurity
                .csrf().disable()
                .oauth2Login()
                .and()
                .authorizeExchange().anyExchange().authenticated();
        return httpSecurity.build();
    }
}

Keycloak Client

Comment From: sjohnr

Hi @straurob. Thanks for your interest in the project!

I've added a comment for possible resources to check out to work through the CORS issue. For reference, it's not required to cross-post from Stack Overflow as the team monitors the spring-security tag regularly, and it's best to ask for help working through issues like this one over there. I'm going to close this issue as a question for stack overflow.

Comment From: razvanmazilu99

Hello, @straurob, I have the same problem, and from what I see, there isn't an answer to our problem here on GitHub, even though the issue was closed. I also tried to access the link from @sjohnr's comment, but it is not available anymore. Did you find a solution? Can you help me?

Comment From: straurob

@razvanmazilu99 I managed to find a solution. It is not related to Spring Security, but I will post the approch here anyway for documentation purposes.

The problem, as far as I can tell, was that the frontend application issued requests using the axios library. When such a request hits a secured resource method, Spring Security will answer with a redirect to Keycloak. Redirects won't work in this case as it's the browser itself which needs to actually call http://localhost:8500/oauth2/authorization/keycloak.

I solved this by catching any 401 response in my frontend application and then redirecting the user agent. Something like this:

if (error.response.status === 401) {
    redirect('http://localhost:8500/oauth2/authorization/keycloak');
}

Comment From: razvanmazilu99

@straurob thank you very much!!!

Comment From: uniquejava

@straurob Thank you for your fine compiled question. The way you structuring spring security apps and the way you debug things is quite inspiring to me. I'm researching how to implement the same architecture with new Spring Authorization Server, Spring Cloud Gateway and Vue3. 😃