DEV Community

Cover image for Spring Boot 3.1 JWT Authentication &Authorization: Secure Your APIs Like a ProπŸ”’
Bhushan Nemade
Bhushan Nemade

Posted on

Spring Boot 3.1 JWT Authentication &Authorization: Secure Your APIs Like a ProπŸ”’

What is Authentication? πŸ”

Authentication is the process of verifying the identity of users, ensuring they are who they claim to be before granting access to a system or application. In the context of web applications, this is crucial for protecting sensitive information and resources.

Authentication involves:

  1. User Credentials:πŸ”‘ Users provide credentials, such as usernames and passwords.
  2. Verification Process:βœ”οΈ The system verifies the provided credentials against stored data. Successful verification grants access; otherwise, access is denied.

Spring SecurityπŸƒ
For Java developers building Spring-based applications, Spring Security serves as the de-facto standard for securing their creations. This comprehensive framework provides robust authentication and authorization mechanisms, ensuring your application's data and functionality remain safe from unauthorized access.

JWT (JSON Web Token)Authentication: A Secure and Scalable ApproachπŸ”πŸ“ˆ

JWT stands for JSON Web Token, a self-contained token that contains information about the user and is signed by the server for verification. It has become a popular choice for authorization due to its numerous advantages.

Use Cases: JWTs are widely used in various scenarios:

Public APIs: Securely access public APIs without requiring frequent logins.
Mobile Applications: Store user information and authorization details within the token for offline use.
Single Sign-On (SSO): This allows users to seamlessly access multiple applications with a single login.

Image description

Now, let's delve into the practical application of JWT by implementing it in a Spring Boot application to secure our API endpoints.β›πŸ‘·

Now we will configure the in-memory user and JWT. We will create an API endpoint and secure it using Spring Boot security.

Create a Spring Boot Project

Use Spring Initializr to create a new Spring Boot project with the following dependencies:

For Web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>`
For Security:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Lombok:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
Enter fullscreen mode Exit fullscreen mode

For JWT:

<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> 
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Let's start with creating an endpoint to be secured:

@RestController
@RequestMapping("/home")
public class Controller {
  @Autowired
    private UserService userService;
  @GetMapping("/users")
    public List<User> getUsers(){
      System.out.println("getting users");
      return userService.getUsers();
  }
}
Enter fullscreen mode Exit fullscreen mode

Create a user:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
    private String userId;
    private String name;
    private String email;
}
Create a service class:
@Service
public class UserService {
    private List<User> store=new ArrayList<>();
    public UserService() {
        store.add(new User(UUID.randomUUID().toString(), "Bhushan", "bhushan@gmail.com"));
        store.add(new User(UUID.randomUUID().toString(), "Ramesh", "ramesh@gmail.com"));
        store.add(new User(UUID.randomUUID().toString(), "Suresh", "suresh@gmail.com"));
        store.add(new User(UUID.randomUUID().toString(), "Paresh", "paresh@gmail.com"));
    }
    public List<User> getUsers(){
        return this.store;
    }
}
Enter fullscreen mode Exit fullscreen mode

Create an in-memory user with UserDetailService bean:

@Configuration
public class AppConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails userDetails = User.builder().
                username("Bhushan")
                .password(passwordEncoder().encode("Nemade")).roles("ADMIN").
                build();
        return new InMemoryUserDetailsManager(userDetails);
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration builder) throws Exception {
        return builder.getAuthenticationManager();
    }
}
Enter fullscreen mode Exit fullscreen mode

Start with creating class JWTAthenticationEntryPoint that will be implementing AuthenticationEntryPoint*:* Used to handle authentication-related exceptions. Specifically, it is responsible for returning an unauthorized (401) response to clients who attempt to access protected resources without proper authentication.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        PrintWriter writer = response.getWriter();
        writer.println("Access Denied !! " + authException.getMessage());
    }
}

Enter fullscreen mode Exit fullscreen mode

Create a JWT helper class: The helper class is often created to encapsulate the logic related to the generation, parsing, validation, and manipulation of JWTs in a more modular and maintainable way. It provides a set of methods and utilities to interact with JWTs.

@Component
public class JwtHelper {
    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
    private String secret = "afafasfafafasfasfasfafacasdasfasxASFACASDFACASDFASFASFDAFASFASDAADSCSDFADCVSGCFVADXCcadwavfsfarvf";
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }
    private String doGenerateToken(Map<String, Object> claims, String subject) {

        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

Enter fullscreen mode Exit fullscreen mode

Create a JwtAuthenticationFilter class which will extend OncePerRequestFilter: Extending the OncePerRequestFilter class for a JWT authentication filter in Spring Security is a common approach to ensure that the filter is only executed once per request. This helps prevent unnecessary duplicate processing and ensures that the filter logic is applied consistently.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
   private Logger logger = LoggerFactory.getLogger(OncePerRequestFilter.class);
    @Autowired
    private JwtHelper jwtHelper;
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestHeader = request.getHeader("Authorization");
        logger.info(" Header :  {}", requestHeader);
        String username = null;
        String token = null;
        if (requestHeader != null && requestHeader.startsWith("Bearer")) {
            token = requestHeader.substring(7);
            try {
                username = this.jwtHelper.getUsernameFromToken(token);
            } catch (IllegalArgumentException e) {
                logger.info("Illegal Argument while fetching the username !!");
                e.printStackTrace();
            } catch (ExpiredJwtException e) {
                logger.info("Given jwt token is expired !!");
                e.printStackTrace();
            } catch (MalformedJwtException e) {
                logger.info("Some changed has done in token !! Invalid Token");
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            logger.info("Invalid Header Value !! ");
        }
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            Boolean validateToken = this.jwtHelper.validateToken(token, userDetails);
            if (validateToken) {     
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } else {
                logger.info("Validation fails !!");
            }
        }
        filterChain.doFilter(request, response);
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a SecurityConfig class as spring security in the configuration file:

@Configuration
public class SecurityConfig {
    @Autowired
    private JwtAuthenticationEntryPoint point;
    @Autowired
    private JwtAuthenticationFilter filter;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .authorizeRequests().
                requestMatchers("/test").authenticated().requestMatchers("/auth/login").permitAll()
                .anyRequest()
                .authenticated()
                .and().exceptionHandling(ex -> ex.authenticationEntryPoint(point))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a login API to accept the username and password:

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private AuthenticationManager manager;
    @Autowired
    private JwtHelper helper;
    private Logger logger = LoggerFactory.getLogger(AuthController.class);
    @PostMapping("/login")
    public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest request) {
        this.doAuthenticate(request.getEmail(), request.getPassword());
        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getEmail());
        String token = this.helper.generateToken(userDetails);

        JwtResponse response = JwtResponse.builder()
                .jwtToken(token)
                .username(userDetails.getUsername()).build();
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
    private void doAuthenticate(String email, String password) {
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(email, password);
        try {
            manager.authenticate(authentication);
        } catch (BadCredentialsException e) {
            throw new BadCredentialsException(" Invalid Username or Password  !!");
        }
    }
    @ExceptionHandler(BadCredentialsException.class)
    public String exceptionHandler() {
        return "Credentials Invalid !!";
    }
}
Enter fullscreen mode Exit fullscreen mode

Create a JWT Request and JWT Response to receive data and send login details: Creating a JWT request and response involves defining structures for sending and receiving JWTs during the authentication process. Typically, a JWT request is used to send user credentials (e.g., username and password) to the server for authentication, and a JWT response is used to deliver a token upon successful authentication.

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class JwtRequest {
    private  String email;
    private String password;
}

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class JwtResponse {
    private String jwtToken;
    private String username;
}

Enter fullscreen mode Exit fullscreen mode

Test the API using Postman to generate a JWT token, then use that token as a header in subsequent requests to access the protected API and verify successful authentication.

Dive Deeper :
https://www.youtube.com/watch?v=q2l91Ffc_8U&t=3299s
https://springframework.guru/jwt-authentication-in-spring-microservices-jwt-token/

Thank you for reading😊

Top comments (2)

Collapse
 
petergfernandez profile image
Peter Fernandez • Edited

Building user authentication and authorization oneself, as part of the development of a project, is an option. And articles like this are a fantastic resource for showing you how; nice work @bhushan_n 😎 However building user authentication and authorization when it's not ones core focus can lead to a whole lot of unnecessary work - that can also open an application to some unwanted and undesirable security implications. Check out my DEV post here to see how integrating with a SaaS platform - such as Auth0 - can be beneficial for a whole host of reasons πŸ€—

Collapse
 
tbroyer profile image
Thomas Broyer

Sigh, yet another post about adding authentication to your app, and doing so with JWT, given visibility by DEV on social networks.

I was tired if it and wrote a couple posts about it (and I'll tirelessly post them in comments of such posts):