Mastering JWT Authentication & Authorization in Spring Boot 3.1

Written by hacker9126880 | Published 2024/02/29
Tech Story Tags: java-programming | spring | spring-framework | spring-boot | jwt-authentication | spring-security | secure-coding | web-app-security

TLDRAuthentication is pivotal in cybersecurity, verifying user identities before granting access. Explore a detailed guide on securing web applications with Spring Security, leveraging JWT authentication for enhanced protection. Follow step-by-step instructions to implement JWT authentication in Spring Boot projects, fortifying your Java applications against security threats.via the TL;DR App

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:

  • User Credentials:🔑
    • Users provide credentials, such as usernames and passwords.
  • 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.

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>

Lombok:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

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>

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();
  }
}

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", "[email protected]"));
        store.add(new User(UUID.randomUUID().toString(), "Ramesh", "[email protected]"));
        store.add(new User(UUID.randomUUID().toString(), "Suresh", "[email protected]"));
        store.add(new User(UUID.randomUUID().toString(), "Paresh", "[email protected]"));
    }
    public List<User> getUsers(){
        return this.store;
    }
}

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();
    }
}

Start with creating the 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());
    }
}

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));
    }
}

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);
    }
}

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();
    }
}

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 !!";
    }
}

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;
}

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 :

Thank you for reading😊

Also published here.


Written by hacker9126880 | I write about Java, Cloud, K8s
Published by HackerNoon on 2024/02/29