Securing Spring boot applications with JWT: Part 1

A comprehensive guide to Authentication, Authorization and Role based access control.

Dharshi Balasubramaniyam
12 min readApr 14, 2024
  • In the ever-evolving landscape of web application development, security remains a top priority for developers and businesses alike.
  • Among the plethora of tools and frameworks available, Spring Boot stands out as a popular choice for building robust and scalable applications with minimal configuration.
  • In this digital age, authentication and authorization are fundamental aspects of any web application.
  • Fortunately, Spring Boot provides seamless integration with JSON Web Tokens (JWT), offering a versatile solution for implementing secure authentication and authorization mechanisms.

In this comprehensive guide, we’ll delve into the intricacies of securing Spring Boot applications using JWT. We’ll explore the entire spectrum of authentication, from enabling users to sign up and sign in securely, to implementing role-based access control to protect sensitive routes. I hope, by the end of this guide, you’ll be well-equipped to safeguard your applications and empower your users with a seamless and secure experience. Let’s dive in!

JSON Web Token (JWT)

  • A JSON web token (JWT) is JSON Object which is used to securely transfer information (username, email, roles etc.) over the web (between two parties).
  • It is a way to remember that user is authenticated or logged in.
  • The token is mainly composed of 3 dot separated parts.
[header].[payload].[signature]
  • Header — specifies token type and encryption method.
  • Payload — specifies the user data and info.
  • Signature — verifies token integrity.

How JWT authentication Implemented?

When user logs in, the server issues a JWT.

This token is stored in the user’s browser and sent back with each request to the server.

The picture below clearly shows how it works.

How JWT works?

Now, we are going to discuss how to implement above scenario in programmatically using Spring Security. Without further ado, Let’s start.

Step 1: Setting up spring boot application.

  • Visit spring initializer and fill all the details accordingly as below and at last click on the GENERATE button. Extract the zip file and import it into your IDE.
  • Add below dependencies in pom.xml file.
<dependencies>
// we'll use this dependency to create RESTful API endpoints,
// handle HTTP requests (GET, POST, PUT, DELETE), and return JSON responses.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

// we'll use this dependency to interact with a database,
// define JPA entities (data models), perform CRUD operations,
// and execute custom database queries.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

// we'll use this dependency to establish a connection to
// our MySQL database, execute SQL queries, and manage database transactions.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

// we'll use Lombok annotations (such as @Data, @Getter, @Setter)
// in our Java classes to automatically generate common methods,
// making your code cleaner and more concise.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
  • Update application.properties file
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/jwtExample
spring.datasource.username=your localhost username
spring.datasource.password=your localhost password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
  • Create modals (User, Role)
// User.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "users",
uniqueConstraints = {
@UniqueConstraint(columnNames = "username"),
@UniqueConstraint(columnNames = "email")
})
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;

private String email;

private String password;

private boolean enabled;

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable( name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}

// ERole.java
public enum ERole {
ROLE_USER,
ROLE_ADMIN,
ROLE_SUPER_ADMIN
}

// Role.java
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Enumerated(EnumType.STRING)
@Column(length = 20)
private ERole name;


public Role(ERole name) {
this.name = name;
}

}
  • Create repositories (User, Role)
// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}

// RoleRepository.java
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(ERole name);
}
  • Create a DTO class for send response
@Data
@Builder
public class ApiResponseDto<T> {
private boolean isSuccess;
private String message;
private T response;
}
  • Create Service class
// UserService.java
@Service
public interface UserService {
boolean existsByUsername(String username);
boolean existsByEmail(String email);
void save(User user);
}

// UserServiceImpl.java
@Component
public class UserServiceImpl implements UserService{
@Autowired
UserRepository userRepository;

@Override
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}

@Override
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}

@Override
public void save(User user){
userRepository.save(user);
}
}
  • Create a demo controller to test whatever we did until now.
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/")
public ResponseEntity<ApiResponseDto<?>> Test() {
return ResponseEntity
.status(HttpStatus.OK)
.body(new ApiResponseDto<>(true, "Let's learn spring security with JWT!"));
}
}
  • Test your application.

Ok then. We have created the basic application. So lets move on to the next section.

Step 2: Add dependencies for JWT

  • Add below dependencies in pom.xml.
  <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

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

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
  • Run your application.

Now you can see, Spring Boot’s auto-generated security password, which is used for protecting your application’s endpoints during development. This password is randomly generated each time you start your Spring Boot application and is meant to be used during development only.

Now, if you search for the endpoint “/api/test/”, it will automatically be redirected to a sign in page. Use “user” as username and the generated password as password to sign in to the system.

After the successful login, you can see the result.

It’s important to replace this generated password with your own secure password before deploying your application to production.

To address this message and ensure the security of your application in production, you should customize the security configuration of your Spring Boot application.

This typically involves setting up authentication mechanisms, such as user authentication with JWT (JSON Web Tokens), OAuth, or other authentication providers, and configuring access control rules for your endpoints.

That’s the topic we are going discuss next.

Step 3: Configure spring security and JWT.

Before using spring security, we have to configure several components of spring security in our application.

Below picture shows what are the components we have to configure under spring security.

Spring security architecture
project folder structure.

Now, let’s go through one by one and try to understand what the need of each component is and how to configure them programmatically.

Step 3.1. UserDetails

  • This UserDetailsImpl class is a custom implementation of the UserDetails interface provided by Spring Security.
  • It’s designed to represent the details of a user in your application, specifically tailored for use with JSON Web Tokens (JWT) authentication.
@AllArgsConstructor
@Data
public class UserDetailsImpl implements UserDetails {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
private boolean enabled;

public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());

return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities,
user.isEnabled());
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
  • It contains fields such as id, username, email, password, authorities, and enabled. These fields hold essential information about a user.
  • There’s also a build method that takes a User object (our application's user model) and creates a UserDetailsImpl object from it.
  • isAccountNonExpired, isAccountNonLocked, and isCredentialsNonExpired methods always return true, indicating that user accounts don't expire, get locked, or have credentials that expire.

Step 3.2. UserDetailsService

  • This UserDetailsServiceImpl class is a service class responsible for loading user details from your application's database or data source.
  • It implements the UserDetailsService interface provided by Spring Security.
  • This service class acts as a bridge between Spring Security’s user authentication mechanisms and your application’s user data store.
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;

@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

return UserDetailsImpl.build(user);
}
}
  • It provides a method to load user details by username (email) and constructs a UserDetailsImpl object to represent those details, facilitating user authentication and authorization within your Spring Security-enabled application.
  • loadUserByUsername(String username): This method is part of the UserDetailsService interface and is overridden here to load user details by username (in this case, the user's email address).
  • It queries the UserRepository to find a user by their email address.
  • If a user is not found, it throws a UsernameNotFoundException with an appropriate message.
  • If a user is found, it delegates the construction of a UserDetailsImpl object to the static build method of UserDetailsImpl class, passing the retrieved User object.
  • Finally, it returns the constructed UserDetailsImpl object.

Step. 3.3. JWTUtils

  • This JwtUtils class provides utility methods for working with JSON Web Tokens (JWT) in your Spring Security-enabled application.
  • This utility class simplifies the generation, extraction, and validation of JWT tokens in your Spring Security application, enhancing security and enabling stateless authentication mechanisms.
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

@Value("${app.jwtSecret}")
private String jwtSecret;

@Value("${app.jwtExpirationMs}")
private int jwtExpirationMs;

public String generateJwtToken(Authentication authentication) {

UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

return Jwts.builder()
.setSubject((userPrincipal.getEmail()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}

private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}

public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder().setSigningKey(key()).build()
.parseClaimsJws(token).getBody().getSubject();
}

public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}

return false;
}
}

Method: generateJwtToken(Authentication authentication)

  • Generates a JWT token based on the provided Authentication object.
  • Extracts user details (UserDetailsImpl) from the Authentication object.
  • Constructs a JWT token using the Jwts.builder() method.
  • Sets the subject (usually user’s email), issued at, expiration time, and signs the token with the HMAC SHA-256 algorithm using the secret key.
  • Returns the compact representation of the JWT token.

Method: validateJwtToken(String authToken)

  • Validates the JWT token.
  • Parses the token using the secret key.
  • Catches and logs exceptions for various invalid token scenarios like malformed, expired, unsupported, or empty claims.
  • Returns true if the token is valid, otherwise false.

@Value: Injects values from the application properties file (application.properties). In this case, it injects the JWT secret key (jwtSecret) and token expiration time (jwtExpirationMs).

// add below values in application.properties file
app.jwtSecret= ==================springboot=jwt=example========================
app.jwtExpirationMs=86400000

Step. 3.4. AuthenticationEntryPoint

  • The AuthEntryPointJwt class is an implementation of the AuthenticationEntryPoint interface in a Spring Security-enabled application, used to handle authentication-related exceptions, specifically unauthorized access attempts.
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());

final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
  • commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException): This method is called when an unauthenticated user attempts to access a secured resource.

Step. 3.5. OncePerRequestFilter

  • The AuthTokenFilter class is an implementation of Spring's OncePerRequestFilter interface, whicch intercepts incoming requests, extracts and validates JWT tokens, loads user details, and sets the authentication context based on the extracted information. It plays a crucial role in implementing stateless authentication mechanisms using JWT in a Spring Security-enabled application.
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;

@Autowired
private UserDetailsServiceImpl userDetailsService;

private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);

UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}

filterChain.doFilter(request, response);
}

private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");

if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}

return null;
}
}

doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain): This method is executed for each incoming HTTP request. It performs the following steps:

  • Attempts to extract the JWT token from the request header using the parseJwt method.
  • If a valid JWT token is found and passes validation using jwtUtils.validateJwtToken(jwt), it extracts the username from the token using jwtUtils.getUserNameFromJwtToken(jwt).
  • Loads the UserDetails object associated with the username using userDetailsService.loadUserByUsername(username).
  • Constructs an Authentication object (UsernamePasswordAuthenticationToken) using the retrieved UserDetails, sets it in the SecurityContextHolder, and marks the request as authenticated.

Step. 3.6. WebSecurityConfig

  • This WebSecurityConfig class is a configuration class responsible for configuring Spring Security settings within your application.
@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
@Autowired
UserDetailsServiceImpl userDetailsService;

@Autowired
private AuthEntryPointJwt unauthorizedHandler;

@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}

@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());

return authProvider;
}

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

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth ->
auth.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);

http.authenticationProvider(authenticationProvider());

http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
  • @Configuration: Indicates that this class provides configuration to the Spring application context.
  • @EnableMethodSecurity: Enables Spring Security's method-level security, allowing you to secure individual methods with annotations like @Secured, @PreAuthorize, etc. (will discuss in part 2 of this article)
  • authenticationJwtTokenFilter(): Creates a bean for the AuthTokenFilter class, responsible for filtering and processing JWT tokens in incoming requests.
  • authenticationProvider(): Creates a bean for the DaoAuthenticationProvider, which is responsible for authenticating users based on the provided user details service and password encoder.
  • authenticationManager(AuthenticationConfiguration authConfig): Creates a bean for the AuthenticationManager, which is used for authentication purposes (Login part — will discuss in part 2).
  • passwordEncoder(): Creates a bean for the PasswordEncoder interface, which is used to encode and verify passwords securely.

filterChain(HttpSecurity http): Configures the security filter chain by:

  • Disabling CSRF protection.
  • Configuring exception handling to use the AuthEntryPointJwt as the authentication entry point for unauthorized requests.
  • Setting session management to stateless, indicating that the application will not create or use HTTP sessions for storing user authentication state.
  • Configuring authorization rules, allowing unauthenticated access to /api/auth endpoints (which is responsible for handle requests related to sign up and sign in) and requiring authentication for any other request.
  • Registering the authenticationProvider and authenticationJwtTokenFilter with the security filter chain.

That’s it guys. We have successfully configured spring security in our application.

Now let’s run our application and try to access the same endpoint “/api/test/” as before.

This time we won’t see any auto generated password, since we configured the spring security.

This time you can’t see the specified message in the browser, Instead of that it will show an alert which specifying “Full authentication is required to access this resource”.

Because we did not specify this end point in our filter chain in WebSecurityConfig , it will check for JWT for authentication, since we didn’t send`an JWT with the request, an error message was thrown by AuthenticationEntryPoint. And that’s what we want also…

So, guys in this article we had seen how to configure spring security in our application. These are the facts I have planned to discuss in this article. Let’s explore how to implement sign up, sign in and access recourses with spring security and JWT in part 2 of this article.

Part 2 link:

Securing Spring boot applications with JWT: Part 2

If you found my articles useful, please consider giving it claps and sharing it with your friends and colleagues.

Until we meet again in the second part of this article, keep learning, exploring, and creating amazing things with Java!

Happy coding!

-Dharshi Balasubramaniyam-

--

--

Dharshi Balasubramaniyam

BSc (hons) in Software Engineering, UG, University of Kelaniya, Sri Lanka. Looking for software engineer/full stack developer internship roles.