Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 64

Building a Secure Login with JWT Authentication

Recommended resources

Welcome back! In our previous lesson, we successfully built a secure registration endpoint. Users can now create an account, and their passwords are safely stored as BCrypt hashes. However, the authentication loop is still incomplete—there's no way for a registered user to log in and prove their identity.

Today, we'll bridge that gap by implementing a login endpoint. You will learn how to take a user's submitted credentials, validate them against the stored hash using Spring Security's powerful authentication mechanisms, and upon success, generate a JSON Web Token (JWT). This token will serve as the user's "access card" for all subsequent requests to protected parts of our API.

By the end of this lesson, you will have a fully functional login system that securely authenticates users and issues a JWT, completing the first major part of our application's security implementation.

The Login Authentication Flow

Before we dive into the code, let's outline the process. When a user tries to log in, a sequence of events is triggered within the Spring Security framework. Understanding this flow is key to configuring it correctly.

JWT Authentication Implementation in Spring Boot | Part 1 - Generate JWT Token via /authenticate API

The video "JWT Authentication Implementation in Spring Boot" provides an excellent recap of the JWT flow and the underlying Spring Security architecture.

Watch the initial overview from this section. Focus on the two main parts: The JWT flow diagram, which shows how a client authenticates once to get a token and then uses that token for future requests. The Spring Security architecture diagram, which introduces the key components: AuthenticationManager, AuthenticationProvider, and UserDetailsService. We will be interacting with these components directly.

As the video explains, our goal is to create an /authenticate endpoint. This endpoint will use the AuthenticationManager to verify the user. The manager, in turn, relies on a UserDetailsService to fetch user data from our database and a PasswordEncoder to compare the passwords. Once authentication succeeds, we'll generate the JWT.


Connecting Spring Security to Our User Data

Spring Security needs to know how to find and understand our User entities. We achieve this by providing two crucial beans.

  1. UserDetailsService: This is a functional interface with a single method, loadUserByUsername(String username). Its job is to locate a user by their username. Since our User entity already implements the UserDetails interface (from the previous lesson), we just need to implement the lookup logic.

  2. AuthenticationManager: This is the central component that processes an authentication request. We need to expose it as a bean so we can inject it into our controller.

Let's configure these in our ApplicationConfig class, which we created in the last lesson.

// In ApplicationConfig.java

// ... other imports
import com.example.yourproject.repository.UserRepository;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

@Configuration
public class ApplicationConfig {

    private final UserRepository userRepository;

    public ApplicationConfig(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder()); // Assumes passwordEncoder bean exists
        return authProvider;
    }

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

Let's break this down:

  • userDetailsService(): We provide a lambda implementation that uses our UserRepository to find a user. If the user doesn't exist, it throws a UsernameNotFoundException, which is the standard contract for this interface.
  • authenticationManager(): This boilerplate code correctly retrieves and exposes the default AuthenticationManager from Spring's configuration.
  • authenticationProvider(): This bean explicitly configures the DaoAuthenticationProvider (DAO stands for Data Access Object). We are telling it to use our custom userDetailsService() to find users and our existing passwordEncoder() to check passwords. Spring Security will automatically use this provider with the AuthenticationManager.

Building the JWT Generation Service

With authentication configured, we need a way to create the JWT itself. Spring doesn't provide a built-in JWT implementation, so we'll use a popular third-party library, jjwt. This logic should be encapsulated in its own service.

First, let's add the necessary dependencies to our pom.xml file.

JWT Authentication Implementation in Spring Boot | Part 1 - Generate JWT Token via /authenticate API

The video demonstrates exactly which dependencies are needed and how to create the initial JwtUtil class.

Follow the steps shown from this part of the video. You will need to add three dependencies to your pom.xml: jjwt-api, jjwt-impl, and jjwt-jackson. After adding them, remember to reload your Maven project to download the new libraries. Then, create a new util or service package and add a JwtService.java class inside it.

Now, let's implement the token generation logic inside our new JwtService. A JWT is essentially a signed JSON object containing "claims" about the user. Key claims include:

  • Subject (sub): Who the token is about (e.g., the username).
  • Issued At (iat): When the token was created.
  • Expiration (exp): When the token becomes invalid.

The token is then signed with a secret key to prevent tampering. This secret key must be kept private on the server.

JWT Authentication Implementation in Spring Boot | Part 1 - Generate JWT Token via /authenticate API

This segment provides a detailed walkthrough of implementing the generateToken method using the jjwt library.

Watch the implementation from this section. Pay close attention to how the builder pattern is used to set the subject, issued-at time, expiration time, and most importantly, how the token is signed with a secret key using an algorithm like HS256.

Here is a complete JwtService.java implementation based on the video, adapted to be a Spring @Service.

package com.example.yourproject.service;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {

    // You should store this in application.properties and make it much longer!
    private final String SECRET_KEY = "afafasfafafasfasfasfafacasdasfasxASFACASDFACASDFASFASFDAFASFASDAADSCSDFADCVSGCFVADXCcadwavfsfarvf";
    private final long JWT_EXPIRATION = 1000 * 60 * 60; // 1 hour

    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return Jwts.builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // We will need these methods in the next lesson
    public String extractUsername(String token) {
        // ... implementation for next lesson
        return null; 
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        // ... implementation for next lesson
        return true;
    }

    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

Important: The SECRET_KEY here is hardcoded for simplicity. In a real-world application, this should be externalized into your application.properties and be a long, randomly generated string.


Creating the Login Endpoint

We now have all the pieces. Let's assemble them in our AuthenticationController and AuthenticationService.

  1. Create DTOs: We need a DTO for the login request and one for the response.

    // Create LoginUserDto.java
    package com.example.yourproject.dto;
    public record LoginUserDto(String username, String password) {}
    
    // Create LoginResponseDto.java
    package com.example.yourproject.dto;
    public record LoginResponseDto(String token, long expiresIn) {}
    
  2. Update AuthenticationService: Add a login method.

    // In AuthenticationService.java
    
    // ... imports for AuthenticationManager, etc.
    import com.example.yourproject.dto.LoginUserDto;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    
    @Service
    public class AuthenticationService {
        // ... existing fields (userRepository, passwordEncoder)
    
        private final AuthenticationManager authenticationManager;
        private final JwtService jwtService; // Inject the new service
    
        public AuthenticationService(
            UserRepository userRepository,
            PasswordEncoder passwordEncoder,
            AuthenticationManager authenticationManager, // Add to constructor
            JwtService jwtService // Add to constructor
        ) {
            this.userRepository = userRepository;
            this.passwordEncoder = passwordEncoder;
            this.authenticationManager = authenticationManager;
            this.jwtService = jwtService;
        }
    
        public User register(RegisterUserDto input) {
            // ... existing registration logic
        }
    
        public String login(LoginUserDto loginUserDto) {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginUserDto.username(),
                    loginUserDto.password()
                )
            );
    
            User user = userRepository.findByUsername(loginUserDto.username())
                    .orElseThrow(); // Should not happen if authenticate succeeds
            
            return jwtService.generateToken(user);
        }
    }
    

    This login method is the core of our logic. It first asks the AuthenticationManager to authenticate the user. If the username doesn't exist or the password is wrong, the authenticate method will throw an exception, and the request will fail with a 401 Unauthorized status. If it succeeds, we fetch the user and generate a token.

  3. Update AuthenticationController: Add the /login endpoint.

JWT Authentication Implementation in Spring Boot | Part 1 - Generate JWT Token via /authenticate API

The video demonstrates how to create the controller endpoint, use the AuthenticationManager, and then call the JWT utility to generate and return the token.

Watch these clips to see how the controller is wired: * Creating the controller and request DTO. * Using AuthenticationManager to authenticate. * Calling the generateToken method and returning the result.

Following this pattern, let's add the method to our `AuthenticationController`:
```java
// In AuthenticationController.java

// ... imports for LoginUserDto, LoginResponseDto

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

    // ... existing authService field and constructor
    private final JwtService jwtService; // We need this to get expiration time

    public AuthenticationController(AuthenticationService authService, JwtService jwtService) {
        this.authService = authService;
        this.jwtService = jwtService;
    }

    @PostMapping("/register")
    public ResponseEntity<User> register(@RequestBody RegisterUserDto registerUserDto) {
        // ... existing code
    }
    
    @PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login(@RequestBody LoginUserDto loginUserDto) {
        String token = authService.login(loginUserDto);
        
        LoginResponseDto loginResponse = new LoginResponseDto(token, jwtService.getJwtExpiration()); // Assumes getJwtExpiration() exists in JwtService
        
        return ResponseEntity.ok(loginResponse);
    }
}
```
*(Note: You'll need to add a simple getter for the `JWT_EXPIRATION` constant in your `JwtService` to make the code above work).*

Testing the Login Endpoint

You're all set! Run your application and use a tool like Postman to test the flow.

  1. Register a User: First, ensure you have a user by sending a POST request to http://localhost:8080/auth/register with a username and password.
  2. Test Successful Login: Send a POST request to http://localhost:8080/auth/login with the correct credentials. You should get a 200 OK response with the JWT in the body.
  3. Test Failed Login: Send the same request but with an incorrect password. You should get a 401 Unauthorized response.
  4. Inspect the Token: Copy the token from the successful response. Go to the website jwt.io. Paste your token into the "Encoded" box. You'll see the decoded payload, including the sub (username) and exp (expiration timestamp).

JWT Authentication Implementation in Spring Boot | Part 1 - Generate JWT Token via /authenticate API

This final clip shows exactly how to use jwt.io to inspect your newly generated token.

Watch how the presenter pastes the token and verifies the payload. You can also paste your own SECRET_KEY into the "Verify Signature" section to confirm the signature is valid.


Conclusion

Congratulations! You have successfully built a complete authentication endpoint. We've configured Spring Security's core components, integrated a robust JWT generation service, and tied it all together in a clean, layered architecture.

Key Takeaways:

  • The AuthenticationManager is the central component for processing login requests in Spring Security.
  • We must provide a UserDetailsService bean to teach Spring Security how to load our application's users.
  • JWTs are generated using a library like jjwt, and contain claims (like subject and expiration) signed with a secret key.
  • The login flow involves first authenticating with the AuthenticationManager, and only then generating and returning a JWT.

We now have a token, but it doesn't grant access to anything yet. All our other endpoints are still protected, and they don't know how to validate this token. In our next lesson, we will solve this by creating and applying a JWT validation filter that will intercept requests, check the token, and establish the user's identity for the duration of the request.