Common Backend Vulnerabilities
Authentication & Authorization
Spring Security JWT authentication
So far, we've used Spring Security's built-in login. Now we connect it to the JWT approach you learned earlier this week. Quick reminder of the flow (you already know why — here we wire up how):
1. POST /auth/login (username + password)
└─► verify credentials ─► create a signed JWT ─► return it to the client
2. Client stores the token and sends it on every request:
Authorization: Bearer <token>
3. On each request, a filter validates the token and tells Spring Security who the user is.
We need five pieces. Let's build them.
UserDetailsServiceSpring Security needs to load users from your database (the User table and repository from your earlier chapters). You give it a UserDetailsService — a single method that takes a username and returns the user's security details.
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public AppUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) {
AppUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // the BCrypt hash stored in your DB
.roles(user.getRole()) // e.g. "USER" -> Spring stores it as ROLE_USER
.build();
}
}
<aside> 💭
UserDetails is Spring Security's view of a user (username, hashed password, authorities). It's separate from your Jdbc AppUser entity on purpose — your entity is about your domain, UserDetails is about security.
</aside>
You need a PasswordEncoder (BCrypt, from the hashing chapter) so Spring can compare the submitted password against the stored hash, and an AuthenticationManager to perform the actual check.
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
When both a UserDetailsService and a PasswordEncoder bean exist, Spring Boot automatically wires them together, so the AuthenticationManager knows how to look users up and verify their passwords.
This endpoint verifies the username/password and, on success, hands back a JWT. authenticationManager.authenticate(...) does the heavy lifting — it loads the user via your UserDetailsService and checks the BCrypt password. It throws if the credentials are wrong (which Spring turns into a 401).
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService; // your token helper from the JWT chapter
public AuthController(AuthenticationManager authenticationManager, JwtService jwtService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
UserDetails user = (UserDetails) authentication.getPrincipal();
String token = jwtService.generateToken(user); // sign a JWT (JWT chapter)
return new LoginResponse(token);
}
public record LoginRequest(String username, String password) {}
public record LoginResponse(String token) {}
}
This is the bridge between JWT and Spring Security. It's a filter (remember section 3) that runs on every request, reads the Authorization: Bearer ... header, validates the token, and — if valid — places an Authentication into the SecurityContextHolder. From that point on, Spring Security treats the request as authenticated, and all your role/method rules just work.
We extend OncePerRequestFilter, which guarantees the filter runs exactly once per request.
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.*;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws Exception {
String authHeader = request.getHeader("Authorization");
// No Bearer token? Don't block here — let the request continue.
// The authorization rules decide whether the endpoint needed a login.
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7); // strip "Bearer "
String username = jwtService.extractUsername(token); // from the JWT chapter
// Only set authentication if we found a user and nobody is authenticated yet.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
var authentication = new UsernamePasswordAuthenticationToken(
userDetails, // the principal (who the user is)
null, // no credentials — the token already proved identity
userDetails.getAuthorities()); // the user's roles/authorities
// This is the key line: tell Spring Security the request is authenticated.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response); // continue down the chain
}
}
Finally, update the SecurityFilterChain to slot the JWT filter in before Spring Security's built-in username/password filter, and configure the chain for a token API:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable()) // see note below
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // no server-side sessions
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Two settings specific to a JWT API, and why (the concepts come from earlier chapters):
sessionManagement(... STATELESS) — A JWT carries the user's identity, so the server doesn't need to remember anyone between requests. We tell Spring Security not to create or use HTTP sessions. Every request is authenticated fresh from its token.csrf(... disable()) — CSRF protection defends cookie/session-based browser logins. A JWT sent in the Authorization header isn't automatically attached by the browser the way a cookie is, so the CSRF attack doesn't apply here. (If you ever store the token in a cookie, this changes — revisit the CSRF chapter.)Once the JWT filter has put the Authentication into the SecurityContextHolder, you can ask "who is the current user?" anywhere in your code. There are two ways.
Option A — @AuthenticationPrincipal (preferred in controllers). Spring injects the current user straight into your handler method:
@RestController
@RequestMapping("/api")
public class ProfileController {
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal UserDetails currentUser) {
return Map.of(
"username", currentUser.getUsername(),
"roles", currentUser.getAuthorities()
);
}
}
Option B — SecurityContextHolder (works anywhere, e.g. in a service). Read the Authentication directly:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // the logged-in username
var authorities = auth.getAuthorities(); // their roles/authorities
A couple of practical notes:
UserDetails. If you need your own AppUser entity (with the database id, email, etc.), you can make a custom principal, but UserDetails is enough to start.id or role sent in the request body to decide identity — always read it from the authenticated Authentication. The whole point of the token is that the server, not the client, decides who the caller is.Here's the full lifecycle of one authenticated request in our app:

Every concept in this chapter has a place in that picture: the filter chain intercepts the request, the JWT filter authenticates it, the SecurityFilterChain bean and method annotations authorize it, and the SecurityContextHolder lets your code know who's calling.
<aside> ⚠️
Heads-up on outdated tutorials: Spring Security's API changed a lot between version 5 and 6. If a tutorial uses WebSecurityConfigurerAdapter, antMatchers, or authorizeRequests, it's outdated — the current equivalents are a SecurityFilterChain bean, requestMatchers, and authorizeHttpRequests. Stick to the patterns in this chapter.
</aside>
The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.