Intermediate · Secure

Enforce Permissions, CORS, and CSRF Correctly

Separate origin policy, cross-site request forgery defenses, URL rules, and method-level authorization.

Primary source: Spring Security 6.5 Reference Architecture

45 min

Lesson 3 of 5

1d streak

The Architectural Question

Confused 401/403 responses, blanket CORS wildcards, and CSRF disabled by habit each trade correctness for a moment of convenience.

What this guide proves

  • Return 401 for missing or invalid authentication and 403 for insufficient authority
  • Keep CSRF enabled for browser flows using automatically attached credentials
  • Model authorities around actions, not UI labels

RBAC vs ABAC & Declarative Method Security with @PreAuthorize

Securing endpoints solely at the HTTP URL level is fragile. Modern applications pair URL authorization with declarative Method Security evaluated directly on service and controller methods using SpEL (Spring Expression Language).

OrderService.java
package com.example.order;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    // Role-based authorization:
    @PreAuthorize("hasRole('ADMIN')")
    public void purgeCancelledOrders() { ... }

    // Authority / Permission-based authorization:
    @PreAuthorize("hasAuthority('orders:approve')")
    public void approveOrder(String orderId) { ... }

    // Attribute-Based Access Control (ABAC): Inspects method argument against principal:
    @PreAuthorize("#order.customerId == authentication.name or hasRole('ADMIN')")
    public void cancelOrder(Order order) { ... }
}

Enable Method Security in Spring Boot 3+

Add @EnableMethodSecurity to any @Configuration class. By default, this enables @PreAuthorize and @PostAuthorize while disabling legacy @Secured and jsr250 annotations.

Active Recall Checkpoint

What is the difference between hasRole("ADMIN") and hasAuthority("ADMIN") in Spring Security?

Hint: What prefix does hasRole look for automatically?

Show answer & explanation

hasRole("ADMIN") automatically prepends the prefix ROLE_, looking for the granted authority ROLE_ADMIN. hasAuthority("ADMIN") looks for the exact string "ADMIN" without modification.

CORS Protocol Mechanics: Preflights, Headers & Safe Configuration

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts web pages from making HTTP requests to a different domain, port, or protocol than the origin serving the web page. CORS is enforced by the browser, NOT by the server.

CorsConfig.java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://app.example.com", "https://admin.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Correlation-Id"));
    config.setAllowCredentials(true);
    config.setMaxAge(Duration.ofHours(1)); // Cache preflight response in browser

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

The Wildcard + Credentials Security Prohibition

The W3C CORS specification forbids setting Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true. Browsers will reject the response immediately to prevent malicious third-party sites from reading authenticated session cookies.

Active Recall Checkpoint

Why does a browser send an HTTP OPTIONS preflight request before sending a POST request with an Authorization header?

Hint: Is Authorization considered a CORS-safelisted request header?

Show answer & explanation

No. Any request with non-safelisted headers (such as Authorization or Content-Type: application/json) triggers a preflight OPTIONS request so the browser can verify whether the server permits cross-origin access before sending the actual data.

CSRF Protection: Cookie Token Repository vs Stateless APIs

Cross-Site Request Forgery (CSRF) occurs when a malicious website tricks a user's browser into executing unwanted actions on an application where the user is currently authenticated via cookies.

Architectural Boundary · Do Not Confuse

Concept A

Ambient Credential Authentication (Cookies, Basic Auth, Client Certificates): Vulnerable to CSRF because browsers automatically attach ambient credentials to cross-origin requests. CSRF protection MUST BE ENABLED using the synchronizer token pattern (such as CookieCsrfTokenRepository).

Concept B

Explicit Credential APIs (Header-Only Bearer Tokens): Immune to CSRF only if the application accepts credentials exclusively via non-ambient HTTP request headers (like Authorization: Bearer) that browsers never attach automatically. Setting SessionCreationPolicy.STATELESS does not alone justify disabling CSRF if cookie or basic credentials remain accepted.

CSRF risk is dictated by how credentials are sent by browsers, not whether the server stores sessions in memory. Disable CSRF only when credentials require explicit client-side JavaScript attachment that malicious third-party pages cannot trigger automatically.

Active Recall Checkpoint

When building a single-page application (React/Angular) authenticated via session cookies, how should Spring Security expose the CSRF token?

Hint: How does JavaScript read the cookie?

Show answer & explanation

Configure CookieCsrfTokenRepository.withHttpOnlyFalse(). This allows JavaScript to read the XSRF-TOKEN cookie value and copy it into the X-XSRF-TOKEN HTTP header on mutating requests.

Deliberate Failure Lab: Diagnosing CORS Preflight 403 Forbidden

One of the most frequent frontend-backend integration crashes is receiving 403 Forbidden on an OPTIONS preflight request from a web browser.

browser-console.log
Access to XMLHttpRequest at 'https://api.example.com/api/orders' from origin 'https://app.example.com'
has been blocked by CORS policy: Response to preflight request doesn't pass access control check:
It does not have HTTP ok status (Status 403 Forbidden).

Root Cause Analysis

In Spring Security, the preflight OPTIONS request carries no Authorization header. If http.cors() is not enabled in SecurityFilterChain, Spring Security intercepts the OPTIONS request in AuthorizationFilter and rejects it with 403 Forbidden before CORS processing ever runs!

The Fix

For browser clients that call the API from another origin, enable CORS in the SecurityFilterChain and register a narrowly scoped CorsConfigurationSource bean. This lets CORS processing run before authorization.

Active Recall Checkpoint

Why does an HTTP preflight OPTIONS request never contain an Authorization header?

Hint: Does the browser send user credentials during preflight checks?

Show answer & explanation

The browser intentionally omits credentials and authorization headers during the preflight probe to verify server CORS policies anonymously before committing user credentials.

Runtime Architecture & Flow

Decision canvas

Evaluate the choice across three constraints

These are comparison criteria, not sequential steps. A strong answer states which constraint dominates in the current system.

CRITERION 01

Return 401 for missing or invalid authentication and 403 for insufficient authority

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 02

Keep CSRF enabled for browser flows using automatically attached credentials

Ask how each option changes this guarantee, its operational cost, and its failure mode.

CRITERION 03

Model authorities around actions, not UI labels

Ask how each option changes this guarantee, its operational cost, and its failure mode.

Decision prompt: Lock one endpoint behind `orders:approve` and test it with three principals.

Working Implementation

Code Walkthrough

Lock one endpoint behind `orders:approve` and test it with three principals.

MethodSecurityCorsLab.java
package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.stereotype.Service;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;

@Configuration
@EnableMethodSecurity
public class MethodSecurityCorsLab {

    @Bean
    SecurityFilterChain configureSecurity(HttpSecurity http) throws Exception {
        return http
                .csrf(csrf -> csrf.disable()) // Disabled for stateless REST bearer APIs
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/api/public/**").permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://dashboard.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
        config.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    @Service
    public static class OrderApprovalService {
        @PreAuthorize("hasAuthority('orders:approve')")
        public void approveOrder(long orderId) {
            // Only principals with orders:approve authority can execute this method
        }
    }
}
@EnableMethodSecurity activates @PreAuthorize and @PostAuthorize annotations across service beans.
hasAuthority('orders:approve') provides granular permission control instead of broad role hierarchies.
CorsConfigurationSource defines allowed origins and headers, preventing browser cross-origin blocking.

Synthesis & Production Verification

Production Acceptance Criteria

  • Differentiate 401 from 403 in tests with distinct principals
  • Keep CSRF enabled for cookie flows and document the reasoning
  • Gate one action behind a fine-grained authority with @PreAuthorize

Architectural & Technical Review

Core Interview Questions

Q01What is the architectural difference between URL authorization and method security in Spring Security?Show answer

URL authorization filters requests at the HTTP boundary before reaching controllers. Method security (@PreAuthorize, @PostAuthorize) enforces fine-grained role, permission, and ABAC rules directly at service methods.

Q02How does CORS preflight OPTIONS request handling interact with Spring Security filter ordering?Show answer

Browsers send preflight OPTIONS requests without credentials. Spring Security must configure .cors() so CorsFilter runs before AuthorizationFilter, preventing preflights from returning 403 Forbidden.

Q03When is it safe to disable CSRF protection in a Spring Boot application?Show answer

CSRF can be safely disabled for stateless REST APIs authenticated purely via Authorization: Bearer headers. It must remain enabled for applications authenticated via browser cookies.

Production Pitfalls

Critical Anti-Patterns

  • Pitfall: Setting Access-Control-Allow-Origin: * with AllowCredentials: true — Prohibited by W3C CORS specification; browsers will reject cross-origin responses that combine wildcard origins with credentials.
  • Pitfall: Blocking preflight OPTIONS requests with 403 Forbidden due to filter order — Always enable .cors() in the SecurityFilterChain so CorsFilter inspects preflight requests before authentication checks.

Primary Documentation & Specifications

Supplementary Module Resources