Week 10

Security Mindset

Threat Modeling Basics

Common Backend Vulnerabilities

Handling Sensitive Data

Hashing vs. Encryption

How to Store Passwords

Authentication & Authorization

Spring Security

Spring Security JWT authentication

Practice

Assignment

Backend Track

Introduction

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.


1. Tell Spring Security where users come from: UserDetailsService

Spring 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>

2. The password encoder and authentication manager

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.

3. The login endpoint

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) {}
}

4. The JWT filter — validating the token on every request

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
    }
}

5. Register the filter and make the API stateless

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):

Extracting user information from the token

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:

Putting it all together

Here's the full lifecycle of one authenticated request in our app:

image.png

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.

Extra Resources

<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>

Further reading


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.