Chapter 7.5☕ 16 min read

Spring Security with Angular Frontend

Building the backend is half the battle. The Angular frontend must play its part too.

01The Concept: Bearer Token Pattern & CORS

The VIP Lounge Wristband Analogy (Revisited):

When you pay at the Goa resort front desk, the receptionist hands you the wristband directly. They don’t mail it to your house. Similarly, when Angular sends a username/password to Spring Boot /login, Spring Boot responds with the JWT directly in the HTTP response body.

Angular takes this token and saves it. The next time Angular asks for data, it attaches the token to the request header: Authorization: Bearer <token>. This is the Bearer Token pattern.

02Technical Explanation
  1. CORS (Cross-Origin Resource Sharing): Angular runs on localhost:4200, Spring Boot on localhost:8080. The browser will block requests between them unless Spring Boot explicitly allows it inside the Security Filter.
  2. Angular HTTP Interceptor: An Angular class that automatically intercepts every HTTP request and injects the Authorization: Bearer header.
  3. Stateless Session: Spring Boot must not create JSESSIONID cookies. It must be STATELESS.
03Full Working Code: The Final Security Config

Here is the complete, modern Spring Security 6 configuration combining JWT, CORS, and Statelessness.

package com.devinhyderabad;

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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. Enable CORS with default settings
.cors(cors -> cors.configurationSource(corsConfigurationSource()))

// 2. Disable CSRF for stateless JWT APIs
.csrf(csrf -> csrf.disable())

// 3. URL Rules
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)

// 4. Tell Spring NOT to create HTTP Sessions
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 5. Add our custom JWT filter before Spring's default password filter
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

// 6. Global CORS Configuration Bean
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:4200"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);

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

How Angular Sends the Token:

// Angular Code (TypeScript)
import { HttpRequest, HttpHandlerFn } from '@angular/common/http';

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
const token = localStorage.getItem('jwt_token');

if (token) {
const clonedReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
return next(clonedReq);
}
return next(req);
}
04Code Walkthrough

SecurityConfig now includes:

  • CORS: corsConfigurationSource() bean explicitly allows localhost:4200. Without this, Angular gets a CORS error in the browser console.
  • SessionManagement(STATELESS): This is critical. Without this, Spring Security still creates a JSESSIONID cookie alongside the JWT, defeating the purpose of stateless auth.
  • addFilterBefore: Our JwtAuthenticationFilter runs before Spring’s built-in UsernamePasswordAuthenticationFilter, ensuring the JWT is processed before any login check.

Angular Interceptor: The authInterceptor function is a functional interceptor (Angular 15+ style). It reads the JWT from localStorage and auto-injects it into every outgoing HTTP request. This saves you from manually adding headers to every API call in your Angular app.

05Why It Matters / Interview Note

Interview Question: “Why do we disable CSRF in a JWT-based Spring Boot application?”

Answer: CSRF (Cross-Site Request Forgery) protection is designed to protect against attacks where a malicious site forces the user’s browser to submit a state-changing request using the user’s existing session cookie. Since our REST API is stateless and uses Bearer tokens (stored in local storage or memory, not cookies), there is no session cookie to hijack. Therefore, CSRF protection is unnecessary and would only break API calls.

Enterprise Note: Storing JWTs in localStorage in Angular is easy but vulnerable to XSS (Cross-Site Scripting) attacks. The most secure enterprise pattern is to store the JWT in an HttpOnly cookie set by the Spring Boot backend, which protects it from JavaScript access. However, this requires configuring SameSite cookie attributes and adds complexity.

Key Takeaways

  • ✅ CORS must be configured on the backend to allow Angular frontend requests
  • ✅ SessionCreationPolicy.STATELESS prevents Spring from creating HTTP sessions
  • ✅ JwtAuthenticationFilter is added before UsernamePasswordAuthenticationFilter
  • ✅ Angular HttpInterceptor auto-injects the Bearer token into every request
  • ✅ localStorage is simple but XSS-vulnerable; HttpOnly cookies are more secure