user26603542
user26603542

Reputation: 1

Getting 401 on public page even if I am authorized

I am developing rest api for forum in spring boot. I have GET mapping on /api/v1/forum/threads, which will return all the threads. But it returns 401 Unauthorized when I try to access it, even if I am authorized.

Thread Controller:

@RestController
@RequestMapping("/api/v1/forum/threads")
public class ThreadController {
    @Autowired
    private ThreadService threadService;
    @GetMapping
    public ResponseEntity<List<Thread>> getAllCategories(){
        List<Thread> threads = threadService.findAll();
        return ResponseEntity.ok(threads);
    }
}

Thread service:

@Service
public class ThreadService {
    @Autowired
    private ThreadRepository threadRepository;

    public Optional<Thread> findById(long id) {
        return threadRepository.findById(id);
    }
    public Optional<List<Thread>> findByName(String keyword){
        return threadRepository.findByNameContainingIgnoreCase(keyword);
    }
    public Optional<List<Thread>> findByCategory(Long id){
        return threadRepository.findByCategoryId(id);
    }
    public List<Thread> findAll(){
        return threadRepository.findAll();
    }
    public Optional<List<Thread>> findByContent(String keyword){
        return threadRepository.findByContentContainingIgnoreCase(keyword);
    }


    public void createThread(Thread thread){
        threadRepository.save(thread);
    }
    public void deleteThread(Long id){
        threadRepository.deleteById(id);
    }
}

Thread Repository:

@Repository
public interface ThreadRepository extends JpaRepository<Thread, Long> {
    Optional<Thread> findById(Long id);
    Optional<List<Thread>> findByNameContainingIgnoreCase(String title);
    Optional<List<Thread>> findByCategoryId(Long categoryId);
    Optional<List<Thread>> findByContentContainingIgnoreCase(String keyword);
}

Security config:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    private JwtFilter jwtFilter;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(request -> request
                        .requestMatchers("/api/v1/forum/threads/**").permitAll()
                        .anyRequest().permitAll()
                )
                .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                .httpBasic(Customizer.withDefaults())
                .build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider(){
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

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

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception{
        AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
        auth.authenticationProvider(authenticationProvider());
        return auth.build();
    }
}

JWT filter:

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Autowired
    JwtUtil jwtUtil;
    @Autowired
    UserDetailsService userDetailsService;

    private static final List<String> PUBLIC_URLS = List.of(
            "/api/v1/forum/categories/**",
            "/api/v1/forum/threads/**"
    );

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            String path = request.getRequestURI();

            // Проверяем, является ли URL публичным
            boolean isPublic = PUBLIC_URLS.stream()
                    .anyMatch(publicUrl -> pathMatcher.match(publicUrl, path));

            if (isPublic) {
                filterChain.doFilter(request, response);
                return;
            }

            if (jwt != null) {
                if (jwtUtil.validateToken(jwt)) {
                    System.out.println(1);
                    String username = jwtUtil.extractUsername(jwt);
                    if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                        System.out.println(2);
                        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities());
                        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                    }
                }
            }
            filterChain.doFilter(request, response);
        }catch (MalformedJwtException ex) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT claims string is empty");
        } catch (Exception ex) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication error");
        }
    }


    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

I thought that problem might be in JWT filter, which is not well writen, but it doesn't seem to be a thing. I have similar Get mapping on /api/v1/forum/categories, which returns all the categories without 401. I tried modifying security config and thread controller. Here is my Categories Controller:


@RestController
@RequestMapping("/api/v1/forum/categories")
public class CategoryController {
    @Autowired
    private ThreadCategoryService threadCategoryService;

    @GetMapping
    public ResponseEntity<List<ThreadCategory>> getAllCategories(){
        List<ThreadCategory> categories = threadCategoryService.findAll();
        return ResponseEntity.ok(categories);
    }

    @GetMapping("/{id}")
    public ResponseEntity<ThreadCategory> getCategoryById(@PathVariable long id) {
        Optional<ThreadCategory> category = threadCategoryService.findById(id);
        if(category.isPresent()){
            return ResponseEntity.ok(category.get());
        }
        return null;
    }

    @GetMapping("/title/{title}")
    public ResponseEntity<Optional<List<ThreadCategory>>> getCategoriesByTitle(@PathVariable String title) {
        Optional<List<ThreadCategory>> categories = threadCategoryService.findByName(title);
        if(categories.isPresent()){
            return ResponseEntity.ok(categories);
        }
        return null;
    }


    @PostMapping
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> createCategory(@RequestBody ThreadCategory category) {
        ThreadCategory created = threadCategoryService.createCategory(category);
        return ResponseEntity.ok(created);
    }
    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> updateCategory(@PathVariable long id, @RequestBody ThreadCategory category) {
        return threadCategoryService.findById(id)
                .map(categoryToUpdate -> {
                    categoryToUpdate.setName(category.getName());
                    categoryToUpdate.setDescription(category.getDescription());
                    threadCategoryService.createCategory(category);
                    return ResponseEntity.ok(category);
                })
                .orElse(ResponseEntity.notFound().build());
    }
    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<?> deleteCategory(@PathVariable Long id) {
        threadCategoryService.deleteCategoryById(id);
        return ResponseEntity.ok("Категория удалена");
    }

}

Upvotes: 0

Views: 51

Answers (1)

Francesco Poli
Francesco Poli

Reputation: 1274

You should also add /api/v1/forum/threads (without any slashes) to the security exceptions in the SecurityConfig and the JwtFilter, currently that configurations set as public only the paths that has a suffix.

Upvotes: 0

Related Questions