My Journey with Spring Security 6 — Part I

Gabriel Mendes
6 min readApr 2, 2023

I had already implemented some authentications in my projects with Spring Security and JWT, but I mostly followed some tutorials, not really understanding the complexities of Spring Security, while trying my best to implement what I consider the best practices.

With Spring 3 and Spring Security 6 a tried to migrate my project and dive into the official documentation of Spring Security, trying to understand the architecture of the vast valley of components that compose Spring Security. And this was my approach.

Domains and UserDetails

Firstly I started with the Domain for my User entity, it CRUDs operations and implementation of UserDetails. I used JPA and Lombook to simplify things. I’m also using an AbstractDomain class, it just implements the Serializable interface, you can directly implement it in your domains.

The User domain:

@Getter
@Setter
@Entity
@Table(name = "tb_user")
public class UserLoreKeeper extends AbstractDomain {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id_user")
private Integer idUserLoreKeeper;
@Column(name = "username", unique = true, nullable = false)
private String dsUsername;
@Column(name = "email", unique = true, nullable = false)
private String dsEmail;
@Column(name = "password", nullable = false)
private String dsPassword;
@Enumerated(EnumType.STRING)
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "tb_role")
@Column(name = "role", nullable = false)
private List<Role> roleList;

}

And the Role I created as an Enum:

public enum Role {

USER;

}

For my implementation of UserDetails, I preferred to create the domain for my UserDetails and my Entity in two separate objects, to maintain everything related to the security module of my project separated from the other modules. For now, I’m ignoring the business logic of the remaining methods for later.

@AllArgsConstructor
public class UserSecurity implements UserDetails {

private UserLoreKeeper userLoreKeeper;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {

return userLoreKeeper.getRoleList().stream()
.map(role -> new SimpleGrantedAuthority(role.name()))
.toList();

}

@Override
public String getPassword() {

return userLoreKeeper.getDsPassword();

}

@Override
public String getUsername() {

return userLoreKeeper.getDsUsername();

}

@Override
public boolean isAccountNonExpired() {

return true;

}

@Override
public boolean isAccountNonLocked() {

return true;

}

@Override
public boolean isCredentialsNonExpired() {

return true;

}

@Override
public boolean isEnabled() {

return true;

}

}

In my CRUD Service of the User object, I created the JPA method to find the user with the username or email, to permit the login with any of these fields. I’m not sure if it’s the best approach for it but worked as I intended.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserLoreKeeperService userLoreKeeperService;

@Override
public UserDetails loadUserByUsername(String dsUsername) throws UsernameNotFoundException {

return new UserSecurity(userLoreKeeperService.findByDsEmailOrDsUsername(dsUsername, dsUsername).orElseThrow(createException(dsUsername)));

}

private Supplier<UsernameNotFoundException> createException(String dsUsername) {

return () -> new UsernameNotFoundException("User " + dsUsername + " not found!");

}

}

For last, I also created a domain for returning the JWT tokens to the client.

@Getter
@AllArgsConstructor
public class Token extends AbstractDomain {

private String dsAccessToken;
private String dsRefreshToken;

}

Filters and Dependencies

That was the most stressful part, I’m not 100% satisfied with the way I did it but was a lot more organized than my previous projects.

I created two different filters. One for authenticating with the username and password and writing the access and refresh JWT token on the response to the client, and another for authenticating with the JWT token received from the client, i.e, one for login and another for authenticating remaining requests. I also created a service for manipulating the JWT token.

For the JWT Service, I used auth0 java-jwt library. The secret, for now, is hardcoded for test purposes.

@Service
public class JWTService {

private static final String SECRET_KEY = "RandomKey";

public String createAcessToken(UserDetails user, String dsIssuer, Integer nrMinutes) {

return JWT.create()
.withSubject(user.getUsername())
.withIssuer(dsIssuer)
.withClaim("roleList", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.withExpiresAt(new Date(System.currentTimeMillis() + nrMinutes * 60 * 1000))
.sign(Algorithm.HMAC256(SECRET_KEY));

}

public String createRefreshToken(UserDetails user, String dsIssuer, Integer nrMinutes) {

return JWT.create()
.withSubject(user.getUsername())
.withIssuer(dsIssuer)
.withExpiresAt(new Date(System.currentTimeMillis() + nrMinutes * 60 * 1000))
.sign(Algorithm.HMAC256(SECRET_KEY));

}

public DecodedJWT decodeToken(String dsToken) {

return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(dsToken);

}

}

For the login filter, I retrieved the username and password from the request as a form and used the JWTService to write the access and refresh tokens on the response after successful authentication. I used a RequestMatcher to indicate what endpoints this Filter should apply. For all authentication filters, I preferred to extend AbstractAuthenticationProcessingFilter instead of a simple OncePerRequestFilter.

public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

@Autowired
private JWTService jwtService;

private static final RequestMatcher antPathLogin = new AntPathRequestMatcher("/auth/login", HttpMethod.POST.name());

public LoginAuthenticationFilter(AuthenticationManager authenticationManager) {
super(antPathLogin, authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

String username = request.getParameter("username");
String password = request.getParameter("password");

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);

return super.getAuthenticationManager().authenticate(authToken);

}

@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {

UserDetails user = (UserDetails) authResult.getPrincipal();

String accessToken = jwtService.createAcessToken(user, request.getRequestURI(), 10);
String refreshToken = jwtService.createRefreshToken(user, request.getRequestURI(), 60);

Token token = new Token(accessToken, refreshToken);

response.setContentType(MediaType.APPLICATION_JSON_VALUE);

new ObjectMapper().writeValue(response.getOutputStream(), token);

}

}

For the JWT Authentication, I needed to overwrite the doFilter method for it to work as I intended. I couldn’t use the successfulAuthentication method without needing to rewrite a bunch of things, so I preferred to save the authentication on the doFilter method. I also used a NegatedRequestMatcher to apply the filter to all endpoints except the authentication ones. For now, I’m not worrying about handling exceptions and writing appropriate error responses to the client, so I just threw BadCredentialExceptions.

public class JWTAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

@Autowired
private JWTService jwtService;

private static final NegatedRequestMatcher antPathLogin = new NegatedRequestMatcher(new AntPathRequestMatcher("/auth/**", HttpMethod.POST.name()));
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(antPathLogin, authenticationManager);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

try {

if (!requiresAuthentication((HttpServletRequest) request, (HttpServletResponse) response)) {

return;

}

SecurityContextHolder.getContext().setAuthentication(attemptAuthentication((HttpServletRequest) request, (HttpServletResponse) response));

} catch (AuthenticationException e) {

unsuccessfulAuthentication((HttpServletRequest) request, (HttpServletResponse) response, e);

} catch (JWTVerificationException e) {

unsuccessfulAuthentication((HttpServletRequest) request, (HttpServletResponse) response, parseException(e));

} finally {

chain.doFilter(request, response);

}

}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (Objects.nonNull(authHeader) && authHeader.startsWith("Bearer ")) {

return createUsernamePasswordToken(jwtService.decodeToken(authHeader.substring(7)));

}

throw new BadCredentialsException("Test");

}

private AuthenticationException parseException(JWTVerificationException e) {

return new BadCredentialsException("Test");

}

private UsernamePasswordAuthenticationToken createUsernamePasswordToken(DecodedJWT decodedJWT) {

String dsUsername = decodedJWT.getSubject();
List<SimpleGrantedAuthority> authorityList = Arrays.stream(decodedJWT.getClaim("roleList").asArray(String.class)).map(SimpleGrantedAuthority::new).collect(Collectors.toList());

return new UsernamePasswordAuthenticationToken(dsUsername, null, authorityList);

}

}

The WebSecurity Configuration

This is where we had the most changes from Spring 3, using a bunch of beans and lambda expressions. I declared all of the necessary beans in one Configuration class, it is way more organized than my previous projects.

I added the two previous authorization filters on the SecurityFilterChain and excluded authorization from auth endpoints.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

private final UserDetailsService userDetailsService;

@Bean
@SneakyThrows
public SecurityFilterChain securityFilterChain(HttpSecurity http) {

http.csrf().disable()
.authorizeHttpRequests((authorize) -> {
authorize.requestMatchers(HttpMethod.POST,"/auth/**").permitAll()
.anyRequest().authenticated();
})
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authenticationProvider(authenticationProvider())
.addFilterAfter(loginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthenticationFilter(), LoginAuthenticationFilter.class);

return http.build();

}

@Bean
public LoginAuthenticationFilter loginAuthenticationFilter() {

return new LoginAuthenticationFilter(authenticationManager());

}

@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter() {

return new JWTAuthenticationFilter(authenticationManager());

}

@Bean
public AuthenticationManager authenticationManager() {

return new ProviderManager(authenticationProvider());

}

@Bean
public AuthenticationProvider authenticationProvider() {

DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());

return daoAuthenticationProvider;

}

@Bean
public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

}

Register and Refreshing Token

That was a very simple part. First, I tried to make a filter to handle those requisitions, but I thought this was too much and demanded a lot of rewriting. I preferred to make a simple controller with services.

For the Controller, I autowired the needed services and used the HttpServeletRequest on the refreshToken method to get the servelet path to use as the issuer of the new tokens.

@RestController
@RequestMapping("/auth")
public class AuthController {

@Autowired
private UserLoreKeeperService userLoreKeeperService;
@Autowired
private RefreshService refreshService;

@PostMapping("/register")
public ResponseEntity<UserLoreKeeper> saveUser(@RequestBody UserLoreKeeper userLoreKeeper) {

return ResponseEntity.status(HttpStatus.CREATED).body(userLoreKeeperService.save(userLoreKeeper));

}

@PostMapping("/refresh")
public ResponseEntity<Token> refreshToken(HttpServletRequest httpServletRequest, @RequestBody Token token) {

return ResponseEntity.ok(refreshService.createTokenFromRefresh(token.getDsRefreshToken(), httpServletRequest.getRequestURI()));

}

}

The RefreshService just decodes the refresh token to get the user and creates new tokens with it.

@Service
public class RefreshService {

@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JWTService jwtService;

public Token createTokenFromRefresh(String dsOldRefreshToken, String dsIssuer) {

DecodedJWT decodedJWT = jwtService.decodeToken(dsOldRefreshToken);
UserSecurity userSecurity = (UserSecurity) userDetailsService.loadUserByUsername(decodedJWT.getSubject());

String dsAccessToken = jwtService.createAcessToken(userSecurity, dsIssuer, 10);
String dsNewRefreshToken = jwtService.createRefreshToken(userSecurity, dsIssuer, 60);

return new Token(dsAccessToken, dsNewRefreshToken);

}

}

Conclusion

I’m still learning Spring Security and I know that it has a lot of holes to fix, but with one day of coding and using purely what the Spring docs provided me this was what I achieve.

I’m still trying to learn the best way to write good error messages for the client, as I can’t use Spring ExceptionHandlers in the FilterChain context and it’s the next step I will take on my project.

I also had a few problems with @EnabledGlobalAutheticatio that I need to fix to use annotations on my controller to handle authorization, but all of this I will leave to part II.

--

--

Gabriel Mendes

A Brazilian programmer and aspirant writer who loves to create.