Getting a Post 403 Forbidden with Spring Boot (VueJS and Axios Frontend)

11,500

Solution 1

When you configure Axios, you can simply specify the header once and for all:

import axios from "axios";

const CSRF_TOKEN = document.cookie.match(new RegExp(`XSRF-TOKEN=([^;]+)`))[1];
const instance = axios.create({
  headers: { "X-XSRF-TOKEN": CSRF_TOKEN }
});
export const AXIOS = instance;

Then (here I assume you use SpringBoot 2.0.0, while it should work also in SpringBoot 1.4.x onward) in your Spring Boot application you should add the following security configs.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // CSRF Token
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
           // you can chain other configs here
    }

}

In this way, Spring will return the token as a cookie in the response (I assume you do a GET first) and you will read it in the AXIOS configuration file.

Solution 2

You should not disable CSRF as per Spring Security documentation except, few special cases. This code will put the CSRF header to VUE. I used vue-resource.

//This token is from Thymeleaf JS generation.
var csrftoken = [[${_csrf.token}]]; 

console.log('csrf - ' + csrftoken) ;

Vue.http.headers.common['X-CSRF-TOKEN'] = csrftoken;

Hope this helps.

Share:
11,500
sm0keman1
Author by

sm0keman1

Updated on June 05, 2022

Comments

  • sm0keman1
    sm0keman1 almost 2 years

    I've been having an issue with CORS and I have tried everything I could find on Stack Overflow and basically anything that I found on Google and have had no luck.

    So I have user authentication on my backend and I have a login page on my frontend. I hooked up the login page with Axios so I could make a post request and tried to login but I kept getting errors like "Preflight request" so I fixed that then I started getting the "Post 403 Forbidden" error.

    It appeared like this:

    POST http://localhost:8080/api/v1/login/ 403 (Forbidden)
    

    Even trying to login using Postman doesn't work so something is clearly wrong. Will be posting class files below

    On my backend, I have a class called WebSecurityConfig which deals with all the CORS stuff:

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsServiceImpl userDetailsService;
    
        @Bean
        public WebMvcConfigurer corsConfigurer() {
            return new WebMvcConfigurerAdapter() {
                @Override
                public void addCorsMappings(CorsRegistry registry) {
                    registry.addMapping("/**")
                            .allowedMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS");
                }
            };
        }
    
        @Bean
        public CorsFilter corsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);
            config.addAllowedOrigin("*");  // TODO: lock down before deploying
            config.addAllowedHeader("*");
            config.addExposedHeader(HttpHeaders.AUTHORIZATION);
            config.addAllowedMethod("*");
            source.registerCorsConfiguration("/**", config);
            return new CorsFilter(source);
        }
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.headers().frameOptions().disable();
            http
                    .cors()
                    .and()
                    .csrf().disable().authorizeRequests()
                    .antMatchers("/").permitAll()
                    .antMatchers("/h2/**").permitAll()
                    .antMatchers(HttpMethod.POST, "/api/v1/login").permitAll()
                    .anyRequest().authenticated()
                    .and()
                    // We filter the api/login requests
                    .addFilterBefore(new JWTLoginFilter("/api/v1/login", authenticationManager()),
                            UsernamePasswordAuthenticationFilter.class);
            // And filter other requests to check the presence of JWT in header
            //.addFilterBefore(new JWTAuthenticationFilter(),
            //       UsernamePasswordAuthenticationFilter.class);
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // Create a default account
            auth.userDetailsService(userDetailsService);
    //        auth.inMemoryAuthentication()
    //                .withUser("admin")
    //                .password("password")
    //                .roles("ADMIN");
        }
    }
    

    On our frontend which is written in VueJS and using Axios to make the call

    <script>
        import { mapActions } from 'vuex';
        import { required, username, minLength } from 'vuelidate/lib/validators';
    
        export default {
            data() {
                return {
                    form: {
                        username: '',
                        password: ''
                    },
                    e1: true,
                    response: ''
                }
            },
            validations: {
                form: {
                    username: {
                        required
                    },
                    password: {
                        required
                    }
                }
            },
            methods: {
                ...mapActions({
                    setToken: 'setToken',
                    setUser: 'setUser'
                }),
                login() {
                    this.response = '';
                    let req = {
                        "username": this.form.username,
                        "password": this.form.password
                    };
    
                    this.$http.post('/api/v1/login/', req)
                    .then(response => {
                        if (response.status === 200) {
                            this.setToken(response.data.token);
                            this.setUser(response.data.user);
    
                            this.$router.push('/dashboard');
                        } else {
                            this.response = response.data.error.message;
                        }
                    }, error => {
                        console.log(error);
                        this.response = 'Unable to connect to server.';
                    });
                }
            }
        }
    </script>
    

    So when I debugged via Chrome's tools (Network), I noticed that the OPTIONS request goes through as shown below:

    OPTIONS request going through

    Here is a picture of the POST error:

    POST Request Error

    Here is another class which handles the OPTIONS request (JWTLoginFilter as referenced in the WebSecurityConfig):

    public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
    
        public JWTLoginFilter(String url, AuthenticationManager authManager) {
            super(new AntPathRequestMatcher(url));
            setAuthenticationManager(authManager);
    
        }
    
        @Override
        public Authentication attemptAuthentication(
                HttpServletRequest req, HttpServletResponse res)
                throws AuthenticationException, IOException, ServletException {
            AccountCredentials creds = new ObjectMapper()
                    .readValue(req.getInputStream(), AccountCredentials.class);
            if (CorsUtils.isPreFlightRequest(req)) {
                res.setStatus(HttpServletResponse.SC_OK);
                return null;
    
            }
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            Collections.emptyList()
    
                    )
            );
        }
    
        @Override
        protected void successfulAuthentication(
                HttpServletRequest req,
                HttpServletResponse res, FilterChain chain,
                Authentication auth) throws IOException, ServletException {
            TokenAuthenticationService
                    .addAuthentication(res, auth.getName());
        }
    }
    
  • Andre
    Andre about 4 years
    I'm using .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyF‌​alse()) but the problem is this just works for GET operations.... Why it doesn't work for PUT, POST and DELETE?