Search code examples
spring-bootkotlinspring-webfluxspring-oauth2

Spring WebFlux Kotlin OAuth2 CORS


So here's the config...

I've got localhost:8080 as my backend, and localhost:3000 as my frontend. Authentication on the backend is handled solely via OAuth2. Basically, user on frontend clicks "Login with Spotify", gets redirected to backend which handles the OAuth2, then redirected back to frontend. All subsequent requests come from the frontend to the backend. (Frontend is in Next.JS BTW)

Here's my issue...

Any request to my backend just throws a CORS error, if I'm not authenticated. (If I'm authenticated, it works as expected). So GET /users/{id}, while not being logged in, returns CORS, instead of just a normal 401. This was okay, as I could just workaround it, however now I can't make a PUT request to an endpoint due to CORS, even while being logged in... I've tried the @CrossOrigin annotation, however that doesn't help. I've ALSO tried .cors().disable() in the SecurityConfig, however I still get CORS errors thrown on the frontend...

Here's what I'm using for Security/WebFlux:

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
       </dependency>
       <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
            <version>5.4.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.3.4.RELEASE</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>

Here's my CorsConfig:

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter("/*")
class CorsConfig: WebFluxConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
                .allowedMethods("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
                .allowCredentials(true)
        super.addCorsMappings(registry)
    }
}

And here's my SecurityConfig:


@EnableWebFluxSecurity
class SecurityConfig(
        @Qualifier("globalErrorHandler")
        private val exceptionHandler: WebExceptionHandler,
        private val authenticationFailureHandler: ServerAuthenticationFailureHandler,
        private val authenticationSuccessHandler: AuthenticationSuccessHandler,
        private val authorizationRequestResolver: ServerOAuth2AuthorizationRequestResolver,
        private val logoutSuccessHandler: LogoutSuccessHandler
) {
    @Bean
    fun securityFilterChain(
            httpSecurity: ServerHttpSecurity
    ): SecurityWebFilterChain = httpSecurity
            .authorizeExchange()
            .pathMatchers("/login", "/register", "/logout").permitAll()
            .anyExchange().authenticated()
            .and()
            .cors().disable()
            .exceptionHandling(::withConfiguration)
            .oauth2Login(::withConfiguration)
            .logout(::withConfiguration)
            .csrf().disable()
            .build()

    fun withConfiguration(spec: ServerHttpSecurity.ExceptionHandlingSpec): Unit =
            spec.accessDeniedHandler(exceptionHandler::handle)
                    .authenticationEntryPoint(exceptionHandler::handle)
                    .run {}

    fun withConfiguration(spec: ServerHttpSecurity.OAuth2LoginSpec): Unit =
            spec.authenticationFailureHandler(authenticationFailureHandler)
                    .authenticationSuccessHandler(authenticationSuccessHandler)
                    .authorizationRequestResolver(authorizationRequestResolver)
                    .run { }

    fun withConfiguration(spec: ServerHttpSecurity.LogoutSpec): Unit =
            spec.logoutUrl("/logout")
                    .logoutSuccessHandler(logoutSuccessHandler)
                    .run { }
}

If you need any more information, please feel free to ask.


Solution

  • Here's my solution, I needed to add this:

        fun corsConfigurationSource(): CorsConfigurationSource {
            val cors = CorsConfiguration()
            cors.allowedOrigins = List.of("*")
            cors.allowedMethods = List.of("*")
            cors.allowedHeaders = List.of("*")
            cors.allowCredentials = true
            cors.maxAge = 3600L
            val source = UrlBasedCorsConfigurationSource()
            source.registerCorsConfiguration("/**", cors)
            return source
        }
    

    Then, in my SecurityWebFilterChain, I needed to make Cors use my configuration:

        @Bean
        fun securityFilterChain(
                httpSecurity: ServerHttpSecurity
        ): SecurityWebFilterChain = httpSecurity
                .cors(::withConfiguration)
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers("/login", "/register", "/logout").permitAll()
                .anyExchange().authenticated()
                .and()
                .exceptionHandling(::withConfiguration)
                .oauth2Login(::withConfiguration)
                .logout(::withConfiguration)
                .build()
    

    And that ::withConfiguration resolves to:

        fun withConfiguration(spec: ServerHttpSecurity.CorsSpec): Unit =
                spec.configurationSource(corsConfigurationSource())
                        .run { }