Search code examples
javareactjsspring-bootrestspring-security

Unable to access Rest endpoints even after successful login in spring security through react form


I am trying to implement login authentication in spring boot ,I have used Spring security and used a react form in the frontend to authenticate , I am using axios to call my /login which works well, the form opens and authenticates perfectly but the problem lies here is that after login is successful i am unable to access other endpoints declared in my spring controller

This is my Spring security config

   @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        return http
                .authorizeHttpRequests(auth->auth
                        .requestMatchers("/","/login/**","/register")
                        .permitAll()
                        .anyRequest()
                        .authenticated())
                .logout(logout->logout
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/")
                        .deleteCookies("JSESSIONID")
                        .invalidateHttpSession(true))
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
                .build();
    }

My login controller

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody RegisterUser registerUser) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(registerUser.getEmail(), registerUser.getPassword())
            );
            if (authentication.isAuthenticated()) {
                return ResponseEntity.ok().body("Login successful");
            } else {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed");
            }
        } catch (AuthenticationException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed");
        }
        
    }

This is my react form

axios.defaults.baseURL="http://localhost:8080"
axios.defaults.withCredentials=true;

const Login=()=>{
    const [email, setEmail]=React.useState('');
    const [password, setpassword]=React.useState('');
    const [error, setError] = React.useState('');
    const navigate =useNavigate();

    const handlesubmit= async (e)=>{
        e.preventDefault();
        try {
            const response = await axios.post("/login" ,{
                email,
                password,
            });

            if(response.status===200){
                console.error(email,password)
                navigate('/');
            }
            else {
                navigate('/logs')
            }
        }
        catch (e){
            console.error(e)
            setError("login failed")
        }
    }

Tried using login->login.form("/login") and loginprocesssurl("/login") in security config yet doesnt work, I have also set my proxy to the port where spring is running yet i have no results

What i want now is after the login is successful ( it is ) i want to access the other endpoints which only authenticated users can access


Solution

  • With the custom endpoint, there is one slight detail missing: SecurityContext is not populated during the login request/response. Without storing it, further requests still will be anonymous despite successful authentication.

    Preferred way is having login logic in the SecurityFilterChain:

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(...)
                .formLogin(it -> it
                    .loginPage("/") // mapped to index.html, letting react handle it
                    .loginProcessingUrl("/login")
                    .successHandler((request, response, authentication) -> {
                        response.setStatus(HttpStatus.OK.value());
                        response.getWriter().print("Login successful");
                    })
                    .failureHandler((request, response, exception) -> {
                        response.sendError(HttpStatus.UNAUTHORIZED.value(), "Login failed");
                    })
                )
                .logout(...)
                ...
                .build();
    }
    

    Why's that? It adds additional UsernamePasswordAuthenticationFilter, which contains the authentication logic of your @PostMapping("/login") endpoint + it stores the SecurityContext in ThreadLocal. Later it is saved by SecurityContextRepository in the other filter down the chain. (Note, it requires explicit save since 6 version)

    If you really want to control the signing in flow in your endpoint (maybe for some side effects), save the context there:

    ...
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody RegisterUser registerUser) {
    try {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(registerUser.getEmail(), registerUser.getPassword())
                );
                if (authentication.isAuthenticated()) {
    
                    // storing context in ThreadLocal, later will be saved in SecurityContextPersistenceFilter
                    SecurityContext context = SecurityContextHolder.createEmptyContext();
                    context.setAuthentication(authentication);
                    SecurityContextHolder.setContext(context);
    
                    return ResponseEntity.ok().body("Login successful");
                } else {
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed");
                }
            } catch (AuthenticationException e) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed");
            }
            
        }
    ...