Understanding and Implementing CORS in Spring Boot with Kotlin

December 21, 2024 4 min read Intermediate

In the modern web development landscape, building secure and efficient applications often involves communication between different domains. However, this cross-origin communication isn't straightforward due to browser security restrictions. This is where CORS (Cross-Origin Resource Sharing) comes into play. In this comprehensive guide, we'll dive deep into CORS, understand why it's necessary, and learn how to implement it correctly in a Spring Boot application using Kotlin.

What is the Same-Origin Policy?

Before we dive into CORS, it's crucial to understand why it exists in the first place. Browsers implement a fundamental security concept called the Same-Origin Policy. This policy restricts how a document or script loaded from one origin can interact with resources from other origins.

An origin consists of three parts:

  • Protocol (http/https)
  • Host (domain name)
  • Port number

For example, if your web application is hosted at https://myapp.com, it can't make direct AJAX calls to https://api.myapp.com because they have different hosts, even though they're part of the same domain family.

// This request from https://myapp.com to https://api.myapp.com would fail:
fetch('https://api.myapp.com/users')
    .then(response => response.json())
    .catch(error => console.error('Error:', error));

This restriction exists for good reasons. Consider this scenario:

  1. You log into your banking website at bank.com
  2. In another tab, you visit a malicious website
  3. Without the Same-Origin Policy, that malicious site could make requests to bank.com using your authenticated session

Enter CORS

CORS is a mechanism that allows servers to specify which origins are permitted to read that information from a web browser. It extends and adds flexibility to the Same-Origin Policy.

Types of Cross-Origin Requests

  1. Simple Requests
    • Must use GET, HEAD, or POST methods
    • Only allowed headers are:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type (limited to specific values)
    • No need for preflight requests
  2. Preflight Requests
    • Browser sends an OPTIONS request first
    • Server must respond with appropriate CORS headers
    • Actual request only proceeds if preflight is successful
  3. Credentialed Requests
    • Include cookies or HTTP authentication
    • Require specific configuration on both client and server
    • More restrictive CORS rules apply

Implementing CORS in Spring Boot with Kotlin

Let's look at different ways to implement CORS in a Spring Boot application.

1. Global CORS Configuration

@Configuration
class CorsConfig {
    @Bean
    fun corsFilter(): CorsFilter {
        val source = UrlBasedCorsConfigurationSource()
        val config = CorsConfiguration().apply {
            // Allow credentials
            allowCredentials = true
            
            // Allow specific origins
            setAllowedOrigins(listOf("https://trusted-frontend.com"))
            
            // Allow specific headers
            addAllowedHeader("Authorization")
            addAllowedHeader("Content-Type")
            addAllowedHeader("Accept")
            
            // Allow specific methods
            addAllowedMethod("GET")
            addAllowedMethod("POST")
            addAllowedMethod("PUT")
            addAllowedMethod("DELETE")
            addAllowedMethod("OPTIONS")
            
            // How long the response to the preflight request can be cached
            maxAge = 3600L
        }
        source.registerCorsConfiguration("/**", config)
        return CorsFilter(source)
    }
}

2. Controller-Level CORS

@CrossOrigin(
    origins = ["https://trusted-frontend.com"],
    allowedHeaders = ["Authorization", "Content-Type"],
    methods = [RequestMethod.GET, RequestMethod.POST],
    maxAge = 3600
)
@RestController
@RequestMapping("/api/users")
class UserController {
    @GetMapping
    fun getUsers(): List<User> {
        // Implementation
    }
}

3. Method-Level CORS

@RestController
@RequestMapping("/api/users")
class UserController {
    @CrossOrigin(origins = ["https://trusted-frontend.com"])
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): User {
        // Implementation
    }
}

Security Best Practices

1. Never Use Wildcards in Production

// DON'T DO THIS IN PRODUCTION
config.addAllowedOrigin("*")

// Instead, specify exact origins
config.setAllowedOrigins(listOf(
    "https://your-frontend.com",
    "https://admin.your-frontend.com"
))

2. Handle Multiple Origins Correctly

@Configuration
class CorsConfig {
    @Bean
    fun corsFilter(): CorsFilter {
        val source = UrlBasedCorsConfigurationSource()
        val config = CorsConfiguration()
        
        val allowedOrigins = listOf(
            "https://prod.yourapp.com",
            "https://stage.yourapp.com",
            "http://localhost:3000"
        )
        
        config.apply {
            allowCredentials = true
            setAllowedOrigins(allowedOrigins)
            addAllowedHeader("*")
            addAllowedMethod("*")
        }
        
        source.registerCorsConfiguration("/**", config)
        return CorsFilter(source)
    }
}

3. Environment-Specific Configuration

@Configuration
class CorsConfig(
    @Value("\${cors.allowed-origins}")
    private val allowedOrigins: List<String>,
    
    @Value("\${cors.allowed-methods}")
    private val allowedMethods: List<String>
) {
    @Bean
    fun corsFilter(): CorsFilter {
        val source = UrlBasedCorsConfigurationSource()
        val config = CorsConfiguration().apply {
            allowCredentials = true
            setAllowedOrigins(allowedOrigins)
            setAllowedMethods(allowedMethods)
            addAllowedHeader("*")
        }
        source.registerCorsConfiguration("/**", config)
        return CorsFilter(source)
    }
}

application.yml:

cors:
  allowed-origins:
    - https://prod.yourapp.com
    - https://stage.yourapp.com
  allowed-methods:
    - GET
    - POST
    - PUT
    - DELETE
    - OPTIONS

Testing CORS Configuration

1. Unit Testing

@SpringBootTest
@AutoConfigureMockMvc
class CorsConfigurationTest {
    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun `should allow requests from trusted origin`() {
        mockMvc.perform(options("/api/users")
            .header("Origin", "https://trusted-frontend.com")
            .header("Access-Control-Request-Method", "GET"))
            .andExpect(status().isOk)
            .andExpect(header().string("Access-Control-Allow-Origin", 
                "https://trusted-frontend.com"))
    }

    @Test
    fun `should reject requests from untrusted origin`() {
        mockMvc.perform(options("/api/users")
            .header("Origin", "https://malicious-site.com")
            .header("Access-Control-Request-Method", "GET"))
            .andExpect(status().isForbidden)
    }
}

2. Integration Testing

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CorsIntegrationTest {
    @LocalServerPort
    private var port: Int = 0

    private lateinit var restTemplate: RestTemplate

    @BeforeEach
    fun setup() {
        restTemplate = RestTemplate()
    }

    @Test
    fun `should handle CORS preflight request`() {
        val headers = HttpHeaders()
        headers.add("Origin", "https://trusted-frontend.com")
        headers.add("Access-Control-Request-Method", "GET")
        
        val request = RequestEntity<Void>(headers, HttpMethod.OPTIONS, 
            URI("http://localhost:$port/api/users"))
        
        val response = restTemplate.exchange(request, String::class.java)
        
        assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
        assertThat(response.headers["Access-Control-Allow-Origin"])
            .contains("https://trusted-frontend.com")
    }
}

Troubleshooting Common CORS Issues

1. Missing Required Headers

If you're seeing errors like:

Access to XMLHttpRequest at 'https://api.yourapp.com' from origin 'https://yourapp.com' 
has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Check your CORS configuration:

@Bean
fun corsFilter(): CorsFilter {
    val source = UrlBasedCorsConfigurationSource()
    val config = CorsConfiguration().apply {
        // Make sure all necessary headers are configured
        allowCredentials = true
        setAllowedOrigins(listOf("https://yourapp.com"))
        addAllowedHeader("*")
        addAllowedMethod("*")
        // Add exposed headers if needed
        setExposedHeaders(listOf("Content-Disposition"))
    }
    source.registerCorsConfiguration("/**", config)
    return CorsFilter(source)
}

2. Credentials Issues

If using credentials (cookies, HTTP authentication), make sure:

  1. Frontend configuration:
fetch('https://api.yourapp.com/data', {
    credentials: 'include'
})
  1. Backend configuration:
config.allowCredentials = true
// Important: Cannot use wildcard with credentials
config.setAllowedOrigins(listOf("https://specific-origin.com"))

Monitoring CORS

1. Logging Configuration

@Component
class CorsLoggingFilter : OncePerRequestFilter() {
    private val logger = LoggerFactory.getLogger(CorsLoggingFilter::class.java)

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val origin = request.getHeader("Origin")
        if (origin != null) {
            logger.info("CORS request from origin: $origin, method: ${request.method}")
        }
        filterChain.doFilter(request, response)
    }
}

2. Metrics Collection

@Component
class CorsMetricsFilter : OncePerRequestFilter() {
    @Autowired
    private lateinit var meterRegistry: MeterRegistry

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val origin = request.getHeader("Origin")
        if (origin != null) {
            meterRegistry.counter("cors.requests",
                "origin", origin,
                "method", request.method
            ).increment()
        }
        filterChain.doFilter(request, response)
    }
}

Conclusion

CORS is a crucial security feature that helps protect web applications from unauthorized cross-origin access while allowing legitimate cross-origin communication. When implementing CORS in a Spring Boot application:

  1. Always specify exact origins in production
  2. Use environment-specific configurations
  3. Implement proper testing
  4. Monitor CORS-related issues
  5. Handle credentials correctly
  6. Keep security best practices in mind

By following these guidelines and understanding the underlying concepts, you can implement CORS securely and effectively in your Spring Boot applications.

Remember that CORS is just one layer of security, and it should be used in conjunction with other security measures like CSRF protection, secure session management, and proper authentication/authorization mechanisms.

Latest Articles

Role-Based Access Control (RBAC) in Spring Security with Kotlin

Role-Based Access Control (RBAC) in Spring Security with Kotlin

4 min read Security · rest

Master Role-Based Access Control (RBAC) in Spring Boot applications using Kotlin with practical examples, from basic setup to advanced configurations with method-level security

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

Implementing API Key Authentication in Spring Boot with Kotlin: A Complete Guide (2024)

4 min read Security · rest

Learn how to implement secure API key authentication in Spring Boot with Kotlin, including rate limiting, monitoring, and production-ready best practices - complete with practical examples and ready-to-use code

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

Why You Should Disable CSRF Protection for REST APIs in Spring Boot (And How to Do It Right)

2 min read Security · rest

Learn why CSRF protection isn't necessary for most REST APIs and how to properly disable it in Spring Boot while maintaining security. Includes code examples and best practices