JWT Mechanism in Spring Security 6 (without WebSecurityConfigurerAdapter)

Aziz Kale
Javarevisited
Published in
10 min readMay 17, 2024

JWT is an important concept in authenticating and authorizing access to an application. In a Java application, especially after migrating to Spring Security 6, you may need to create or update your security configurations without using WebSecurityConfigurerAdapter.

In this article, I will discuss the issue of JWT in a Java Maven project. I will be using Spring Security 6 and of course without the WebSecurityConfigurerAdapter.

I will use PostgreSQL as the database.

My IDE will be IntelliJ IDEA and you should have the basic understanding of Spring Boot Security to better benefit from this article.

I create my project at the start.spring.io:

1. Spring Security Dependency

And now I add firstly Spring Security dependency in the pom.xml file:

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.0.3</version>
</dependency>

I add the PostgreSQL configuration codes below to the application.propertiesfile:

spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=postgres
spring.datasource.password=<your passsword here>
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update

After that once you run the project, you can see a generated password in the console:

But we don’t need that as we will customize the security settings of the project.

2. Security Filter Chain

The security filter is the first thing a request goes through when you send an HTTP request to a server. Spring Security handles incoming requests under the security layer and ensures that these requests are authenticated and authorized. When using JWT, incoming requests are expected to contain JWT. These tokens are validated by Spring Security and if the validation is successful, the request is processed or rejected based on the user’s authorizations.

Now I add the SecurityConfigclass under the security package. The file looks like this:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

public static final String ADMIN = "admin";
public static final String USER = "user";

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
(authz) -> authz.requestMatchers("/api/some-path-here/**")
.permitAll()
.requestMatchers(HttpMethod.GET)
.permitAll()
.requestMatchers(HttpMethod.PUT)
.hasAuthority(USER)
.requestMatchers(HttpMethod.POST)
.permitAll()
.anyRequest()
.authenticated());
http.csrf(c -> c.disable());
return http.build();
}
}

@Configuration marks the class as a Spring Boot configuration class and with @EnableWebSecuritywe enable Spring Security’s web features.

I defined two roles, but they can differ according to your app’s needs.

By requestMatchersmethod allows you to allow or restrict paths within the application or http-method whichever you want, and to set permissions on them.

Project Structure

First, I want to add some classes and interfaces to the application that I can use to manage users and roles.

I add lombok to the pom.xml that provides less code:

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>

Now I create the controller, service, entities, dao, dto packages and associated classes under those packages.

I need two entities. One is for users another is for roles:

Under the package modelI add the classAppUser:

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
@Entity
@Table(name = "app_user")
public class AppUser implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Integer id;
@Column(name = "username", columnDefinition = "text", unique = true)
private String username;
@Column(name = "password")
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> roles = new ArrayList<>();
}

And theRoleclass:

@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
}

I named the user class as AppUser. Because the Useris used by UserDetails in UserDetailsService. We will use this in our CustomUserDetailsServiceclass.

And now I add repositories of AppUser and Role classes under the package dao.

public interface AppUserRepository extends JpaRepository<AppUser, Integer> {
Optional<AppUser> findByUsername(String username);
Boolean existsByUsername(String username);
}
public interface RoleRepository extends JpaRepository<Role, Integer> {
Optional<Role> findByName(String name);
}

These repositories are interfaces that provide methods for accessing data.

I add the registerDto class at the dto package that I will use as a data-transfer-object:

@Data
public class RegisterDto {
private String username;
private String password;
private String role;
}

Now I create the controller class under the package controllerand add the register method:

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

private AppUserRepository appUserRepository;
private RoleRepository roleRepository;
private PasswordEncoder passwordEncoder;
@Autowired
public AuthController(AppUserRepository appUserRepository,
RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
this.appUserRepository = appUserRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}

@PostMapping("register")
public ResponseEntity<String> register(@RequestBody RegisterDto registerDto) {

if (appUserRepository.existsByUsername(registerDto.getUsername())) {
return new ResponseEntity<>("Username is taken!", HttpStatus.BAD_REQUEST);
}

AppUser appUser = new AppUser();
appUser.setUsername(registerDto.getUsername());
appUser.setPassword(passwordEncoder.encode((registerDto.getPassword())));

Role roles = roleRepository.findByName(registerDto.getRole()).get();
appUser.setRoles(Collections.singletonList(roles));

appUserRepository.save(appUser);

return new ResponseEntity<>("User registered success!", HttpStatus.OK);
}
}

PasswordEncoder: This refers to the interface provided by Spring Security. It defines methods for encoding and decoding passwords securely.

And I have to add this to my SecurityConfig class:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String ADMIN = "admin";
public static final String USER = "user";

...

@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Now once the app runs, Hibernate(JPA) creates the tables by using entities of the project.

This register method allows you to create new users, assign them roles, and save them in the database. But roles should be in the database before creating a user. I add the roles manually to the PostgreSQL database:

And I send my first request to the Spring Boot server and I get the result below:

3. AuthenticationManager and CustomUserDetailsService

Now I can create thelogin method. But first, I need a LoginDto. Under the dto folder:

@Data
public class LoginDto {
private String username;
private String password;
}

AuthenticationManager and CustomUserDetailsService structures are also required to be able to log in.

AuthenticationManager: This component plays a crucial role in user login. It takes an Authentication object (containing username and password) and attempts to authenticate the user. It relies on a UserDetailsService to retrieve user details and compares the provided credentials with the stored information. If successful, it returns a valid Authentication object.

I just inject this in AuthController and create my login method:

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

...
private AuthenticationManager authenticationManager;

@Autowired
public AuthController(AppUserRepository appUserRepository, RoleRepository roleRepository,
PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
this.appUserRepository = appUserRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}

@PostMapping("register")
public ResponseEntity<String> register(@RequestBody RegisterDto registerDto) {

...
}

@PostMapping("login")
public ResponseEntity<String> login(@RequestBody LoginDto loginDto) {

Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String response = "You have successfully logged in!";
return new ResponseEntity<>(response, HttpStatus.OK);
}
}

For CustomUserDetailsService I create a servicefolder and in it:

@Service
public class CustomUserDetailsService implements UserDetailsService {

private AppUserRepository appUserRepository;

@Autowired
public CustomUserDetailsService(AppUserRepository appUserRepository) {
this.appUserRepository = appUserRepository;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser appUser = appUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Username not found"));
return new User(appUser.getUsername(), appUser.getPassword(), mapRolesToAuthorities(appUser.getRoles()));
}

private Collection<GrantedAuthority> mapRolesToAuthorities(List<Role> roles) {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList());
}
}

UserDeatilsService: This is a core interface in Spring Security responsible for user authentication. Its primary method is loadUserByUsername(String username). This method takes a username and is expected to return an UserDetails object containing user information for authentication.

CustomUserDetailsService: This class extends the UserDetailsServiceinterface and implements its loadUserByUsername method.

mapRolesToAuthorities: This method takes a list of Role objects from the AppUser. It iterates through each Role and creates a corresponding SimpleGrantedAuthority object. These SimpleGrantedAuthority objects represent the user’s authorization decisions.

After that, I add these two structures to the SecurityConfig class.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Autowired
private CustomUserDetailsService userDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
return http.build();
}

...

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}

Now I can go to Postman and test the login method:

Bingo! My register and login endpoints work.

And my project structure seems like this:

Ok, but when I log in, I want to get a JWT, not a string expression.

4. JWT Implementation

For the JWT implementation, I need three dependencies:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>

JWT mechanism:

Firstly, I add the JwtAuthEntryPointunder the security folder:

@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

This is a Spring Security component that handles unauthorized access attempts in a Spring Boot application that utilizes JSON Web Tokens (JWT) for authentication. It implements the AuthenticationEntryPoint interface, which is part of Spring Security’s authentication flow.

It acts as a security checkpoint. If a request lacks a valid JWT, it rejects the request with a 401 (Unauthorized) status code and it can optionally provide an informative message.

Once it is created, it must be injected in SecurityConfigfile:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
...
@Autowired
private JwtAuthEntryPoint authEntryPoint;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...

http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(authEntryPoint));
http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

http.csrf(c -> c.disable());
return http.build();
}
...
}

At this point, I can add sessionManagment and exceptionHandling to the SecurityFilterChain as above.

sessionManagment: This determines security policies related to HTTP sessions. These settings specify how user sessions are managed.

exceptionHandling : This addresses exceptions that may occur during authentication and generates an appropriate response.

And now the JwtGenerator class comes into play. Again under the security folder:

@Component
public class JwtGenerator {

public long JWT_EXPIRATION = 70000;
private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

public String generateToken(Authentication authentication) {
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + JWT_EXPIRATION);

// Extract roles from the authentication object
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());

// Build the token with roles included in claims
String token = Jwts
.builder()
.setSubject(username)
.claim("roles", roles) // Inclusion of roles in claims
.setIssuedAt(new Date())
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512,secretKey).compact();
return token;
}

public String getUsernameFromJWT(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
return claims.getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (Exception ex) {
throw new AuthenticationCredentialsNotFoundException("JWT was exprired or incorrect",
ex.fillInStackTrace());
}
}

public List<String> getRolesFromJWT(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
return claims.get("roles", List.class);
}
}

JWT_EXPIRATION: This is needed to be able to control the expiration time of the token. And it can be changed according to the application.

We have methods here that allow us to get the username, the user’s role list and to validate the token.

And we need a method that gets the token in the requests and validates and handles it. I write the codes below in the file JwtAuthenticationFilter under the folder security:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtGenerator tokenGenerator;
@Autowired
private CustomUserDetailsService customUserDetailsService;


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getJWTFromRequest(request);
if (StringUtils.hasText(token) && tokenGenerator.validateToken(token)) {
String username = tokenGenerator.getUsernameFromJWT(token);

List<String> roles = tokenGenerator.getRolesFromJWT(token);
List<GrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority(role.replace("ROLE_", "")))
.collect(Collectors.toList());

UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

}
filterChain.doFilter(request, response);
}

private String getJWTFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}

The doFilterInternal method:

  • is called for each HTTP request.
  • gets the JWT by the getJWTFromRequest.
  • validates it by validateToken method.
  • extracts the username and roles from the JWT (if it’s valid).
  • loads user details based on the username and roles by loadUserByUserName.
  • creates a UsernamePasswordAuthenticationToken using the loaded user details and roles.
  • sets the created authentication details into the security context (SecurityContextHolder).

Finally, it proceeds to the next filter chain.

I also add this to the SecurityConfig class. And after all the injections and additions in the SecurityClass it seems so:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
public static final String ADMIN = "admin";
public static final String USER = "user";

@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthEntryPoint authEntryPoint;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authz) -> authz.requestMatchers("/api/**")
.permitAll()
.requestMatchers(HttpMethod.GET)
.permitAll()
.requestMatchers(HttpMethod.DELETE)
.hasAuthority(ADMIN)
.requestMatchers(HttpMethod.POST)
.permitAll()
.requestMatchers(HttpMethod.PUT)
.hasAuthority(USER)
.anyRequest()
.authenticated());

http.exceptionHandling((exceptions) -> exceptions.authenticationEntryPoint(authEntryPoint));
http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.csrf(c -> c.disable());
return http.build();
}

@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}

Normally, when I log in, I get a ‘login successful’ message. But now I can get a JWT instead. For that, I create a response dto under the file dto:

@Data
public class AuthResponseDto {
private String accessToken;
private String tokenType = "Bearer ";

public AuthResponseDto(String accessToken) {
this.accessToken = accessToken;
}
}

And then I go the AuthController class and inject the JwtGenerator and update the login method:

@RestController
@RequestMapping("/api/auth")
public class AuthController {
...
private JwtGenerator jwtGenerator;

@Autowired
public AuthController(AppUserRepository appUserRepository, RoleRepository roleRepository,
PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager,
JwtGenerator jwtGenerator) {
...
this.jwtGenerator = jwtGenerator;
}

...

@PostMapping("login")
public ResponseEntity<AuthResponseDto> login(@RequestBody LoginDto loginDto) {

Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtGenerator.generateToken(authentication);
return new ResponseEntity<>(new AuthResponseDto(token), HttpStatus.OK);
}
}

And Now I can test it:

Conclusion

In this article, we discussed about JWT in Spring Security. We used a real database to save users and roles and to retrieve users.

We built a JWT mechanism in a Maven project with the necessary dependencies. And talked about how this mechanism works and what structures we need to build this mechanism.

The JWT mechanism in Spring Security is a little complex and some “imports” in the project files need to be seen in the example project in github. You can access the project via this link.

Thanks very much for your time and attention to my article.

--

--

Aziz Kale
Javarevisited

Highly Motivated, Passionate Full Stack Developer | EMM-IT Co. | Web: azizkale.com