Skip to main content

Securing Your Spring Boot App with JWT Authentication

· 8 min read
Huseyin BABAL

Introduction

This article dives into securing a Spring Boot application using JSON Web Tokens (JWT) for authentication. We'll explore Spring Security, JWT fundamentals, and then implement a secure API with user registration, login, and access control. Our data will be persisted in a PostgreSQL database using Spring Data JPA.

Why Spring Security?

Spring Security is an industry-standard framework for securing Spring applications. It offers comprehensive features for authentication, authorization, and access control. By leveraging Spring Security, we can efficiently manage user access to our API endpoints.

JWT Authentication Explained

JWT is a token-based authentication mechanism. Unlike traditional session-based methods, JWT stores user information in a compact, self-contained token. This token is sent with every request, allowing the server to verify the user's identity without relying on server-side sessions.

Here's a breakdown of JWT's benefits:

  • Stateless: Removes the need for session management on the server.
  • Secure: Employs digital signatures to prevent tampering.
  • Flexible: Can be configured with various claims to store user information.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

Dependencies

Be sure you have the following dependencies installed by using your favourite dependency management tool e.g. maven, gradle.

pom.xml
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Enabling Spring Web Security

In order to enable Spring Web Security, you need to configure it in your SecurityConfig.java file as shown below.

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private static final String[] AUTH_WHITELIST = {
"/api/v1/auth/login",
"/api/v1/auth/register"
};

private final JwtAuthFilter jwtAuthFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

Line 2: Add @EnableWebSecurity to the SecurityConfig class to protect the API endpoints.

Line 6: Allow requests from the /api/v1/auth/login and /api/v1/auth/register endpoints without authentication.

Line 16: Disable CSRF protection, since JWT authentication is stateless.

Line 24: Set the session creation policy to STATELESS to ensure sessions are not maintained.

Line 25: Add the JwtAuthFilter to the security filter chain before the UsernamePasswordAuthenticationFilter. We will explain JwtAuthFilter class soon.

JWT Auth Filter

In order to enable JWT authentication, you need to configure it in your JwtAuthFilter.java file as shown below.

JwtAuthFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}

final String authorizationHeader = request.getHeader("Authorization");
final String jwtToken;
final String email;

if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

jwtToken = authorizationHeader.substring(7);
email = jwtService.extractEmail(jwtToken);

if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtService.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}

Line 10: Do not apply JWT auth filter for /api/v1/auth endpoints.

Line 24: Extract JWT token from the Authorization header. Its format is Bearer <token>, that's why it is substring(7).

Line 25: Extract email from the JWT token using JwtService which we will take a look at in the next section.

Line 28-32: Validate the JWT token using JwtService, load user details using UserDetails from UserDetailsService and store the authentication in SecurityContextHolder.

Implementing JWTService

This class contains all JWT related functionalities as shown below.

JwtService.java
@Service
public class JwtService {

@Value("${jwt.secret}")
private String secret;

public String extractEmail(String jwtToken) {
return extractClaim(jwtToken, Claims::getSubject);
}

public <T> T extractClaim(String jwtToken, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(jwtToken);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String jwtToken) {
return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(jwtToken).getPayload();
}

private SecretKey getSigningKey() {
byte [] bytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(bytes);
}

public boolean validateToken(String jwtToken, UserDetails userDetails) {
final String email = extractEmail(jwtToken);
return email.equals(userDetails.getUsername()) && !isTokenExpired(jwtToken);
}

private boolean isTokenExpired(String jwtToken) {
return extractExpiration(jwtToken).before(new Date());
}

private Date extractExpiration(String jwtToken) {
return extractClaim(jwtToken, Claims::getExpiration);
}

public String generateToken(User u) {
return createToken(u.getEmail());
}

private String createToken(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(getSigningKey())
.compact();
}
}

Line 5: This is the secret key used to sign JWT tokens. This should be carefully protected, it is not something that we can share or expose publicly. All the other functions are self-explanatory.

UserDetailsService

UserDetailsService is design for showing spring boot security authentication how to load user details from database as shown below.

UserDetailsService.java
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;


@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.map(user -> User.builder().username(user.getEmail())
.password(user.getPassword())
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}

Until this point, we have only focused on JWT authentication. However, how we will generate JWT tokens in the next section? What is its use-case?

Registering User

Before generating JWT token to authenticate the user, we need to register the user. We will use AuthController to register user.

AuthController.java
@RestController
@RequestMapping(path = "api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;


@PostMapping(path = "/register")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void register(@RequestBody RegisterRequest registerRequest) {
authService.register(registerRequest);
}

@PostMapping(path = "/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
return ResponseEntity.ok(authService.login(loginRequest));
}
}

In above controller, we are using AuthService to register and login user. AuthService uses UserRepository to interact database for user related operations.

AuthService.java
@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

public void register(RegisterRequest registerRequest) {
User u = User.builder()
.email(registerRequest.getEmail())
.password(bCryptPasswordEncoder.encode(registerRequest.getPassword()))
.firstName(registerRequest.getFirstName())
.lastName(registerRequest.getLastName())
.build();
userRepository.save(u);
}

public String login(LoginRequest loginRequest) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
User u = userRepository.findByEmail(loginRequest.getEmail()).orElseThrow(() -> new EntityNotFoundException("User not found"));
return jwtService.generateToken(u);

}
}

Line 10: Register user by using the details provided in the request payload. The bCryptPasswordEncoder is used to hash the password before storing it in the database.

Line 21: The login operation is done through authenticationManager since it knows how to validate username and password.

Restricted Access to UserController

You can see a sample endpoint implementation for user object.

UserController.java
@RestController
@RequestMapping(path = "api/v1")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}

@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
}

Assume you registered a new user with email admin password ssshhhh. Then in order to generate a JWT token, you can use the following curl request.

curl -X POST -H "Content-Type: application/json" \
-d '{"email": "admin", "password": "ssshhhh"}' http://localhost:8080/api/v1/auth/login

It will return a JWT token, which you can use to authenticate the user. Store it somewhere.

Now in order to access restricted user endpoint, you can use the following curl request.

curl -X GET -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/users

Conclusion

This hands-on tutorial equipped you with the knowledge to implement JWT Authentication in your Spring Boot application. We explored user registration, login, and access control, leveraging Spring Security and JPA for data persistence. By following these steps and customizing the code examples to your specific needs, you can secure your API endpoints and ensure authorized user access. Remember to prioritize security best practices. Here are some additional points to consider:

  • Secret Key Management: Store your JWT secret key securely in environment variables or a dedicated secret management service. Never expose it in your codebase.
  • Token Expiration: Set a reasonable expiration time for JWT tokens to prevent unauthorized access due to compromised tokens.
  • Error Handling: Implement proper error handling mechanisms for invalid or expired tokens to provide informative feedback to users.
  • Advanced Features: Explore advanced JWT features like refresh tokens for longer-lived sessions and role-based access control (RBAC) for granular authorization. With JWT authentication in place, your Spring Boot application is well on its way to becoming a secure and robust platform. Deploy it with confidence, knowing that user access is properly controlled.
tip

You can find the complete source code for this project on GitHub.