Search code examples
javaspring-bootspring-securityjwt

Spring Jwt issue authentication issue


I'm currently developing the backend for a Learning Management System (LMS) website and I'm encountering an issue with JWT token authentication

when try to log in to

After successfully logging into the system using a username and password, I obtain a JWT token without any issues

i get a forbidden here

However, when I attempt to authenticate using this JWT token, I consistently receive a "Forbidden" error.

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final AccUserDetailsService userService;
    private final JwtAuthenticationTokenFilter filterService;

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("api/staff/**").hasAnyRole("STAFF","ADMIN")
                .requestMatchers("api/student/**").hasRole("STUDENT")
                .anyRequest().authenticated());
        http.addFilterBefore(filterService, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers("/logging", "/welcome","api/signup","/api/authenticate");
    }

}

this is my security configuration class

@Configuration
public class JwtAuthenticationProvider {
    @Value("${api.jwtSecret}")
    private String secret;

    private final int TOKEN_VALIDITY = 5 * 60 * 60;

    public String generateToken(String userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails);
    }

    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() + TOKEN_VALIDITY * 1000))
                .signWith(getSigningKey(),SignatureAlgorithm.HS256).compact();

    }

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

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

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    private <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.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));

    }

}

and this is my jwtconfiguration class

@Component
@RequiredArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private final JwtAuthenticationProvider tokenService;
    private final AccUserDetailsService accUserDetailsService;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");
        String username = null;
        String jwt = null;

        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
            jwt = authorizationHeader.substring(7);
            username = tokenService.getUsernameFromToken(jwt);
        }
        if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.accUserDetailsService.loadUserByUsername(username);
            if(tokenService.validateToken(jwt,userDetails)){
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails,null,userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request,response);
    }
}

filter service provided here

@RestController
@RequiredArgsConstructor
public class TestController {

    private final AccountService accountService;
    private final AuthService authService;
    
    @GetMapping("/api/admin/")
    public String admin(){
        return ("<h1>Welcome admin<h1>");
    }

    @PostMapping("/api/signup")
    public String createAccount(@RequestBody SignUpRequest signUpRequest){
        accountService.signup(signUpRequest);
        return "test";
    }

    @PostMapping("/api/authenticate")
    public ResponseEntity<APIResponse<?>> createAccount(@RequestBody LoginRequest loginRequest) throws AuthenticationException {
        LoginResponse loginResponse = authService.userAuthenticate(loginRequest);
        return ResponseEntity.ok(new APIResponse<>(ResponseCode.SUCCESS_LOGIN,loginResponse));
    }
    
}

Controller class

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.dsm</groupId>
    <artifactId>dsm</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>dsm</name>
    <description>Driving </description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <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>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>0.11.5</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

I've gone through my codebase and configurations, but I'm still unable to resolve the issue. If anyone has experience with this specific combination of Java and Spring versions, I'd really appreciate your assistance in troubleshooting this issue.


Solution

  • Your UserDetails class contains authorities.

    You are using UsernamePasswordAuthenticationToken with your UserDetails authorities.

    If you are setting it in your SecurityContextHolder.

    try using hasAuthority or hasAnyAuthority instead of hasRole or hasAnyRole.

    .requestMatchers("api/student/**").hasAuthority("STUDENT") 
                    .requestMatchers("api/staff/**").hasAnyAuthority("STAFF","ADMIN")
    

    Make sure your role like STUDENT ,STAFF are included in your UserDetails's authorities.

    Hope it helps.