Skip to main content

Welcome to a Grasp lesson

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

Lesson 69

Building a JWT Authentication Endpoint

Recommended resources

Hello! In our previous lesson, we successfully configured Spring Security to authenticate users against our MySQL database. While functional, our reliance on HTTP Basic authentication is not ideal for the stateless REST APIs we aim to build. Each request still requires the client to send credentials, which is inefficient and less flexible than modern token-based approaches.

Today, we will evolve our security model by implementing a dedicated login endpoint that generates a JSON Web Token (JWT). You'll leverage your past experience with JWTs from your MERN project, but this time, you'll see how to integrate them within the robust Spring Security framework. By the end of this lesson, you will have a controller that can authenticate a user's credentials and issue a signed JWT, setting the stage for a fully stateless and secure API.

From HTTP Basic to a Token-Based Login Flow

Let's start by visualizing the process we're about to build. The core idea is to create a public endpoint (e.g., /auth/login) that accepts a user's credentials in the request body. If the credentials are valid, the server generates a JWT and returns it to the client. The client can then store this token and use it for subsequent requests to access protected resources.

The diagram below illustrates this login sequence perfectly.

This diagram shows the user login process we are implementing. The client sends credentials via a POST request, the server authenticates them, and upon success, returns a JWT.

To achieve this, we need three key components:

  1. A JWT Service to handle the creation and signing of tokens.
  2. An AuthenticationManager bean to programmatically trigger authentication.
  3. A Controller to expose the login endpoint and orchestrate the process.

Step 1: Adding JWT Dependencies

First, we need to add the necessary libraries for creating and managing JWTs. The jjwt library is a popular and robust choice for Java.

Open your pom.xml file and add the following dependencies. A good practice is to define the version as a property to ensure they are all consistent.

<properties>
    <java.version>17</java.version>
    <jjwt.version>0.11.5</jjwt.version>
</properties>

<dependencies>
    <!-- ... other dependencies ... -->

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>${jjwt.version}</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>${jjwt.version}</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>${jjwt.version}</version>
    </dependency>

    <!-- ... other dependencies ... -->
</dependencies>

After adding these, be sure to reload your Maven project in your IDE.


Step 2: The JwtService

A clean design separates token management logic into its own service. This service will be responsible for generating tokens, and later, for validating them and extracting information.

JWT authentication in Spring Boot 3 with Spring Security 6

This article by Téric Cabrel provides an excellent, modern implementation of a JwtService. We will adopt this pattern for our project.

Please read the section titled "Create the JWT service". Focus on understanding the generateToken and buildToken methods. Notice how it uses a secret key and expiration time read from the application properties.

Now, let's create this service in your project.

  1. In your application.properties file, add the configuration for the JWT secret and expiration time. The secret key must be a secure, long, random string. For development, you can generate one from a site like the one mentioned in the article.

    
    
    
    # application.properties
    
    
    
    
    # JWT Properties
    # Generate a 256-bit HMAC secret key
    security.jwt.secret-key=averylongandsecuresecretkeythatisatleast256bitslongfortesting
    
    
    
    # Expiration time in milliseconds (e.g., 1 hour)
    security.jwt.expiration-time=3600000
    
  2. Create a new class JwtService in your com.example.groupmanager.service package and add the code from the article. For your convenience, the code is adapted here. Note that we've updated the generateToken method to work with our CustomUserDetails class.

    package com.example.groupmanager.service;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import org.springframework.beans.factory.annotation.Value;
    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 {
        @Value("${security.jwt.secret-key}")
        private String secretKey;
    
        @Value("${security.jwt.expiration-time}")
        private long jwtExpiration;
    
        public String extractUsername(String token) {
            return extractClaim(token, Claims::getSubject);
        }
    
        public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
            final Claims claims = extractAllClaims(token);
            return claimsResolver.apply(claims);
        }
    
        public String generateToken(UserDetails userDetails) {
            return generateToken(new HashMap<>(), userDetails);
        }
    
        public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
            return buildToken(extraClaims, userDetails, jwtExpiration);
        }
    
        public long getExpirationTime() {
            return jwtExpiration;
        }
    
        private String buildToken(
                Map<String, Object> extraClaims,
                UserDetails userDetails,
                long expiration
        ) {
            return Jwts
                    .builder()
                    .setClaims(extraClaims)
                    .setSubject(userDetails.getUsername())
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + expiration))
                    .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                    .compact();
        }
    
        public boolean isTokenValid(String token, UserDetails userDetails) {
            final String username = extractUsername(token);
            return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
        }
    
        private boolean isTokenExpired(String token) {
            return extractExpiration(token).before(new Date());
        }
    
        private Date extractExpiration(String token) {
            return extractClaim(token, Claims::getExpiration);
        }
    
        private Claims extractAllClaims(String token) {
            return Jwts
                    .parserBuilder()
                    .setSigningKey(getSignInKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }
    
        private Key getSignInKey() {
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            return Keys.hmacShaKeyFor(keyBytes);
        }
    }
    

Step 3: Exposing the AuthenticationManager

To authenticate a user programmatically in our controller, we need access to Spring Security's AuthenticationManager. By default, it isn't available as a bean for injection. We must explicitly expose it in our SecurityConfig.

This is a critical configuration step, and Dan Vega's video explains the correct way to do it, avoiding a common but discouraged pattern.

Spring Boot Spring Security JWT: How to authenticate with a username and password

In this video, Dan Vega refactors a Spring Security JWT application to move away from HTTP Basic. We will focus on his explanation of configuring the AuthenticationManager.

Watch the segment from this point. Pay close attention to his reasoning for creating a new DaoAuthenticationProvider to build the AuthenticationManager, rather than getting it from AuthenticationConfiguration. This is the recommended approach for custom authentication flows.

Based on this, let's update your SecurityConfig.java:

  1. First, we need to allow public access to our new login endpoint. We also no longer need HTTP Basic.
  2. Then, we will add the AuthenticationManager bean.

Update your securityFilterChain method and add the authenticationManager bean to your SecurityConfig class:

// In SecurityConfig.java

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.http.SessionCreationPolicy;
// ... other imports

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    // ... your PasswordEncoder bean is already here ...

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    // An alternative way to define the AuthenticationManager
    /*
    @Bean
    public AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(authProvider);
    }
    */
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authz -> authz
                // Allow our new login endpoint to be accessed without authentication
                .requestMatchers("/auth/login").permitAll()
                // Keep the rules from the previous lesson
                .requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/users").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // We will manage the session statelessly because we are using JWT
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
            // We removed .httpBasic() because we are no longer using it

        // In the next lesson, we will add our JWT filter here.
        
        return http.build();
    }
}

Note: The video shows creating a DaoAuthenticationProvider manually. Spring Boot 3 simplifies this; getting the manager from AuthenticationConfiguration is now the standard approach. I've included the manual way in comments for your reference as it helps understand the underlying components.


Step 4: Building the Authentication Controller

Finally, let's create the endpoint that ties everything together. This controller will accept login credentials, use the AuthenticationManager to verify them, and if successful, use the JwtService to generate a token.

Spring Boot Spring Security JWT: How to authenticate with a username and password

Dan Vega continues by creating the controller logic. This is exactly what we need to do.

Watch from here. He creates a LoginRequest record, injects the AuthenticationManager, and uses it to authenticate a UsernamePasswordAuthenticationToken. This is the core logic for our login endpoint.

Let's implement this.

  1. First, create the DTOs (Data Transfer Objects) for the request and response. Create a new package com.example.groupmanager.dto.

    Create LoginRequest.java:

    package com.example.groupmanager.dto;
    
    // Using a record for an immutable data carrier
    public record LoginRequest(String username, String password) {
    }
    

    Create LoginResponse.java:

    package com.example.groupmanager.dto;
    
    public record LoginResponse(String token) {
    }
    
  2. Now, create the AuthController in a new com.example.groupmanager.controller package.

    package com.example.groupmanager.controller;
    
    import com.example.groupmanager.dto.LoginRequest;
    import com.example.groupmanager.dto.LoginResponse;
    import com.example.groupmanager.service.JwtService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/auth")
    public class AuthController {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Autowired
        private JwtService jwtService;
    
        @PostMapping("/login")
        public LoginResponse authenticate(@RequestBody LoginRequest request) {
            // The AuthenticationManager will use the UserDetailsService and PasswordEncoder
            // to validate the credentials.
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            request.username(),
                            request.password()
                    )
            );
    
            // If authentication is successful, the 'authentication' object will be populated.
            // We can now generate a JWT.
            UserDetails userDetails = userDetailsService.loadUserByUsername(request.username());
            String token = jwtService.generateToken(userDetails);
    
            return new LoginResponse(token);
        }
    }
    

Step 5: Test the Login Endpoint

You're ready to test! Restart your application.

  1. Open Postman.
  2. Create a new POST request to http://localhost:8080/auth/login.
  3. Go to the "Body" tab, select "raw", and choose "JSON" from the dropdown.
  4. Enter the credentials for the admin user we created in the last lesson:
    {
        "username": "admin",
        "password": "password"
    }
    
  5. Send the request. If everything is correct, you should receive a 200 OK response with the JWT in the body:
    {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTY3..."
    }
    
  6. Try sending the request with an incorrect password. You should receive a 403 Forbidden error, because Spring Security's exception handling translates the BadCredentialsException to this status by default.

Conclusion

Excellent work! You have successfully created a stateless login endpoint that authenticates users against your database and issues a signed JSON Web Token. This is a massive step forward from HTTP Basic and forms the foundation of modern API security.

Key Takeaways:

  • A dedicated /login endpoint is the entry point for a token-based authentication system.
  • The jjwt library provides a powerful API for creating and managing JWTs in Java.
  • The AuthenticationManager is the key Spring Security component for programmatically authenticating users. It must be exposed as a bean in your security configuration.
  • The login flow involves: receiving credentials, passing them to the AuthenticationManager, and on success, generating a JWT with a JwtService.

We now have a way to get a token, but our application doesn't do anything with it yet. If you try to access a protected endpoint like GET /users, it will still fail. In our next lesson, we will complete the loop by implementing a custom JWT filter. This filter will intercept every incoming request, validate the token from the Authorization header, and establish the user's identity, allowing them to access the resources they are authorized for.