CORS on Spring Boot Applications in Kotlin

Spring Boot with Kotlin provides multiple elegant ways to configure CORS. This guide covers modern, secure implementations for both reactive (WebFlux) and traditional (Spring MVC) applications.

⚠️ Security Warning: Using Access-Control-Allow-Origin: * allows any website to access your resources. Always specify exact origins in production.

Which approach should I use?

  • WebFlux (Reactive) - Use if you have spring-boot-starter-webflux dependency
  • Spring MVC (Traditional) - Use if you have spring-boot-starter-web dependency

Check your build.gradle.kts or pom.xml to determine which stack you're using.

Method 1: Spring Boot WebFlux (Reactive)

For reactive applications using Spring WebFlux:

// CorsConfig.kt
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.CorsWebFilter
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource

@Configuration
class CorsConfig {
    @Bean
    fun corsWebFilter(): CorsWebFilter {
        val corsConfig = CorsConfiguration().apply {
            // Specify allowed origins (NOT wildcard)
            allowedOrigins = listOf(
                "https://example.com",
                "https://app.example.com"
            )

            // Allowed HTTP methods
            allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")

            // Allowed headers
            allowedHeaders = listOf("Content-Type", "Authorization")

            // Enable credentials
            allowCredentials = true

            // Preflight cache duration (seconds)
            maxAge = 86400L
        }

        val source = UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", corsConfig)
        }

        return CorsWebFilter(source)
    }
}

Method 2: Spring MVC (Non-Reactive)

For traditional servlet-based Spring Boot applications:

// CorsConfig.kt
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class CorsConfig : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins(
                "https://example.com",
                "https://app.example.com"
            )
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Content-Type", "Authorization")
            .allowCredentials(true)
            .maxAge(86400)
    }
}

Method 3: Controller-Level with @CrossOrigin

Apply CORS configuration to an entire controller:

// ApiController.kt
import org.springframework.web.bind.annotation.*

// Per-controller CORS
@RestController
@RequestMapping("/api")
@CrossOrigin(
    origins = ["https://example.com", "https://app.example.com"],
    methods = [
        RequestMethod.GET,
        RequestMethod.POST,
        RequestMethod.PUT,
        RequestMethod.DELETE
    ],
    allowedHeaders = ["Content-Type", "Authorization"],
    allowCredentials = "true",
    maxAge = 86400
)
class ApiController {

    @GetMapping("/data")
    fun getData(): Map<String, String> {
        return mapOf("message" to "CORS enabled")
    }

    // Override CORS for specific method
    @CrossOrigin(origins = ["https://public.example.com"])
    @GetMapping("/public")
    fun getPublicData(): Map<String, String> {
        return mapOf("message" to "Public endpoint")
    }
}

Method 4: Method-Level @CrossOrigin

Apply CORS to individual methods for fine-grained control:

// ApiController.kt
@RestController
@RequestMapping("/api")
class ApiController {

    // CORS only on this specific method
    @CrossOrigin(
        origins = ["https://example.com"],
        allowCredentials = "true"
    )
    @PostMapping("/secure")
    fun secureEndpoint(@RequestBody data: Map<String, Any>): Map<String, String> {
        return mapOf("status" to "success")
    }

    // No CORS on this method
    @GetMapping("/internal")
    fun internalEndpoint(): Map<String, String> {
        return mapOf("message" to "Internal only")
    }
}

Method 5: With Spring Security

When using Spring Security, configure CORS in your security configuration:

// SecurityConfig.kt
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

@Configuration
@EnableWebSecurity
class SecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            .cors { cors ->
                cors.configurationSource(corsConfigurationSource())
            }
            .csrf { it.disable() }
            .authorizeHttpRequests { auth ->
                auth.requestMatchers("/public/**").permitAll()
                    .anyRequest().authenticated()
            }

        return http.build()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration().apply {
            allowedOrigins = listOf(
                "https://example.com",
                "https://app.example.com"
            )
            allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
            allowedHeaders = listOf("Authorization", "Content-Type")
            allowCredentials = true
            maxAge = 86400L
        }

        return UrlBasedCorsConfigurationSource().apply {
            registerCorsConfiguration("/**", configuration)
        }
    }
}

Important: When using Spring Security, configure CORS before other security settings like CSRF. Place .cors { } as the first configuration in your security chain.

Method 6: Environment-Based Configuration

Use environment variables or profiles to manage allowed origins across environments:

// CorsConfig.kt
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration
class CorsConfig(
    @Value("\${cors.allowed-origins}")
    private val allowedOrigins: List<String>
) : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/api/**")
            .allowedOrigins(*allowedOrigins.toTypedArray())
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Content-Type", "Authorization")
            .allowCredentials(true)
            .maxAge(86400)
    }
}

application.yml:

cors:
  allowed-origins:
    - https://example.com
    - https://app.example.com

# Or with profiles
---
spring:
  config:
    activate:
      on-profile: production

cors:
  allowed-origins:
    - https://example.com

---
spring:
  config:
    activate:
      on-profile: development

cors:
  allowed-origins:
    - http://localhost:3000
    - http://localhost:5173

Spring Boot/Kotlin Specific Considerations

1. WebFlux vs MVC

How to tell which you're using:

// If you have spring-boot-starter-webflux
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
}
// Use WebFlux approach (Method 1)

// If you have spring-boot-starter-web
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
}
// Use MVC approach (Method 2)

WebFlux (Reactive):

MVC (Traditional):

2. Configuration Precedence

When multiple CORS configurations exist, they are applied in this order (highest to lowest priority):

  1. Method-level @CrossOrigin - Highest priority
  2. Controller-level @CrossOrigin - Medium priority
  3. Global configuration - Lowest priority

More specific configurations override general ones.

3. Kotlin DSL Advantages

Kotlin's DSL makes configuration more readable and concise:

// Clean Kotlin DSL with apply
val corsConfig = CorsConfiguration().apply {
    allowedOrigins = listOf("https://example.com")
    allowedMethods = listOf("GET", "POST")
    allowCredentials = true
}

// vs Java
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedOrigins(Arrays.asList("https://example.com"));
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST"));
corsConfig.setAllowCredentials(true);

Testing Your Configuration

1. Automated Testing with Spring Boot

// CorsTest.kt
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired

@SpringBootTest
@AutoConfigureMockMvc
class CorsTest @Autowired constructor(
    private val mockMvc: MockMvc
) {

    @Test
    fun `should allow whitelisted origin`() {
        mockMvc.perform(
            get("/api/data")
                .header("Origin", "https://example.com")
        )
            .andExpect(status().isOk)
            .andExpect(
                header().string(
                    "Access-Control-Allow-Origin",
                    "https://example.com"
                )
            )
    }

    @Test
    fun `should reject non-whitelisted origin`() {
        mockMvc.perform(
            get("/api/data")
                .header("Origin", "https://evil.com")
        )
            .andExpect(status().isOk)
            .andExpect(
                header().doesNotExist("Access-Control-Allow-Origin")
            )
    }

    @Test
    fun `should handle preflight request`() {
        mockMvc.perform(
            options("/api/data")
                .header("Origin", "https://example.com")
                .header("Access-Control-Request-Method", "POST")
                .header("Access-Control-Request-Headers", "Content-Type")
        )
            .andExpect(status().isOk)
            .andExpect(
                header().string(
                    "Access-Control-Allow-Methods",
                    containsString("POST")
                )
            )
    }
}

Run tests with:

./gradlew test
# or
./mvnw test

3. Enable Spring Boot Logging

Add to application.yml to debug CORS issues:

logging:
  level:
    org.springframework.web.cors: DEBUG
    org.springframework.security.web.cors: DEBUG

Common Pitfalls

Pitfall 1: Using Wildcard with Credentials

This combination is INVALID per CORS specification:

// INVALID - Will fail in browsers
@CrossOrigin(origins = ["*"], allowCredentials = "true")

Must use specific origins:

// VALID
@CrossOrigin(
    origins = ["https://example.com"],
    allowCredentials = "true"
)

Pitfall 2: Wrong Import Packages

Make sure you're using the correct imports for your stack:

// WebFlux (Reactive) - CORRECT
import org.springframework.web.cors.reactive.CorsWebFilter
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource

// MVC (Servlet) - CORRECT
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

Pitfall 3: Missing Spring Security Configuration

If you use Spring Security without CORS configuration, your CORS settings will be ignored. Always configure CORS in your Security Config when using Spring Security.

Testing Your CORS Configuration

For comprehensive testing instructions including curl commands, browser DevTools usage, and troubleshooting common CORS errors, see the CORS Testing Guide.

Additional Resources

Who’s behind this

Monsur Hossain and Michael Hausenblas

Contribute

The content on this site stays fresh thanks to help from users like you! If you have suggestions or would like to contribute, fork us on GitHub.

Buy the book

Save 39% on CORS in Action with promotional code hossainco at manning.com/hossain