Home > database >  Request parameter in Spring Boot is null when submitting JavaScript request but works with normal re
Request parameter in Spring Boot is null when submitting JavaScript request but works with normal re

Time:04-05

I have wasted 10 hours trying to figure out a simple thing.

I have a React front-end and a Spring Boot back-end running on port 8080.

I have my own login form at the front-end.

All I would like in an ideal world is let Spring Boot do its form validation as usual without that ugly bootstrap form.

I have setup a basic in memory authentication just to practice and it just doesn't work

I have two options:

  1. Try to modify formLogin() method in security class to get it to work (Im trying but failing)

  2. Handle authentication in a Rest endpoint and then set the Security context as a rest endpoint instead of in a filter

Here is the code:

Configuration file:

@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private TestFilter testFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("mama")
                .password("mama")
                .roles("AUTH");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //Request authorization
        http
                .authorizeRequests()
                .mvcMatchers("/api/vault").hasRole("AUTH")
                .mvcMatchers("/api/vault/**").hasRole("AUTH")
                .mvcMatchers("/**").permitAll();

        //Disable default authentication behaviour:
        //http.httpBasic().disable();

       http.formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/auth-success")
                .failureUrl("/auth-failure");

        //CSRF Protection:
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setHeaderName("X-CSRF-TOKEN");
        repository.setParameterName("_csrf");
        http.csrf()
                .csrfTokenRepository(repository);

        http.addFilterBefore(testFilter, UsernamePasswordAuthenticationFilter.class);

    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }
}

Default Controller:

@Controller
public class App {

    /**
     * 
     */
    @GetMapping(value = "/{path:[^\\.]*}")
    public String index() {
        return "index";
    }

}

Simple Test API:

@RestController
public class TestApi {

    @GetMapping(value = "/api/get-csrf", consumes = {"application/json"})
    public CsrfTestResponse getCsrf(HttpServletRequest request){

        TestingLombokWorks testingLombokWorks = new TestingLombokWorks("Mama", "Mama is the best!");

        CsrfToken token = new HttpSessionCsrfTokenRepository().loadToken(request);
        CsrfTestResponse testResponse = new CsrfTestResponse();
        testResponse.setCsrfHeaderName(token.getHeaderName());
        testResponse.setCsrfParameterName(token.getParameterName());
        testResponse.setCsrfToken(token.getToken());
        testResponse.setTestingLombokWorks(testingLombokWorks);
        return testResponse;
    }

    @GetMapping("/api/welcome")
    public String publicResource(){
        return "Welcome API!";
    }

    @GetMapping("/api/login-success")
    public String loginSuccess() {
        return "Login Successful!";
    }

    @PostMapping("/api/post-test")
    public CsrfTestResponse postTest(HttpServletRequest request){

        CsrfToken token = new HttpSessionCsrfTokenRepository().loadToken(request);

        CsrfTestResponse testResponse = new CsrfTestResponse();
        testResponse.setCsrfHeaderName(token.getHeaderName());
        testResponse.setCsrfParameterName(token.getParameterName());
        testResponse.setCsrfToken(token.getToken());
        return testResponse;
    }

    @PostMapping("/login")
    public String testLogin(){
        return "Login Works?!";
    }

    @GetMapping("/auth-success")
    public String authSuccess(){
        return "Auth Success!";
    }

    @GetMapping("/auth-failure")
    public String authFailure(){
        return "Auth Failure!";
    }

}

The Filter to test:

@Component
public class TestFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("Username?: "   request.getParameter("username"));
        System.out.println("Password?: "   request.getParameter("password"));
        filterChain.doFilter(request, response);
    }
}

React front-end:

import React from 'react'
import { useForm } from 'react-hook-form'

export default function Login(props) {

    const {register, handleSubmit, formState} = useForm();

    return (
        <form onSubmit={handleSubmit((data, event) => {

                event.preventDefault();

                let formData = new FormData();
                formData.append("username", data.username);
                formData.append("password", data.password);

                let body = new URLSearchParams(formData);

                let str = `username=${data.username}&password=${data.password}`;

                console.log(str);

                fetch('/login', {
                    method: 'POST',
                    credentials: 'include',
                    headers: {
                        'Content-Type': 'application/json',
                        Accept: '*/*',
                        [props.csrfHeaderName]: props.csrfToken
                    },
                    body: str
                })
                .then(response => response.text())
                .then(data => console.log(data))
                .catch(error => console.log(error));
               
            })}
        >
            <legend>Login</legend>
            <div>
                <input 
                    {
                        ...register("username", {
                            value: "",
                            required: true,
                        })
                    } 
                    type="text"
                    placeholder='Username: '
                />
            </div>
            <div>
                <input 
                    {
                        ...register("password", {
                            value: "",
                            required: true,
                        })
                    } 
                    type="password"
                    placeholder='Password: '
                />
            </div>
            <button type='submit'>Login</button>
        </form>
    )
}

I have created a test filter just to test and it is proving my point. When the form is submitted with the normal server side rendered default Spring Boot form, the request works, but when the request is sent with JavaScript it fails.

When I see the logs for the post request for login, I dont see it hitting that endpoint at all, instead it directly goes to /auth-failure:

Username?: null
Password?: null
Username?: null
Password?: null
2022-04-04 15:53:45.960 DEBUG 20327 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.springreact.api.TestApi#authFailure()
2022-04-04 15:53:45.960 DEBUG 20327 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.springreact.api.TestApi#authFailure()
2022-04-04 15:53:45.960 DEBUG 20327 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.springreact.api.TestApi#authFailure()
2022-04-04 15:53:45.961 DEBUG 20327 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : GET "/auth-failure", parameters={}
2022-04-04 15:53:45.961 DEBUG 20327 --- [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.springreact.api.TestApi#authFailure()
2022-04-04 15:53:45.961 DEBUG 20327 --- [nio-8080-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, text/plain, */*, application/json, application/* json, application/json, application/* json]
2022-04-04 15:53:45.961 DEBUG 20327 --- [nio-8080-exec-5] m.m.a.RequestResponseBodyMethodProcessor : Writing ["Auth Failure!"]
2022-04-04 15:53:45.962 DEBUG 20327 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

I would like to know what is the best way to do this simple authentication.

I tried everything imaginable, I tried JSON post body with ObjectMapper, it threw the IOException with no meaningful error message,

Java can be so frustrating and outdated sometimes, when you end up wasting 10 hours getting simple things to work.

CodePudding user response:

Java can be so frustrating and outdated sometimes, when you end up wasting 10 hours getting simple things to work.

This can't be said. It is just that you miss much important info to understand why it is failing.

1st)

http.formLogin()
                .loginPage("/")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/auth-success")
                .failureUrl("/auth-failure");

This is intended to be used with Spring MVC not in scope of Rest Spring Boot application indicated by the use of @RestController. Spring login form is intended to deliver an html form and the system uses the MVC pattern to make the authorization. It does not match your needs where, you have another frontend with another framework doing simple ajax calls.

2nd)

After you understand why (1) fails you have to decide which path you can go down to. You can switch to basic authentication or you can try to implement something more complex like JWT authentication token. There are more solutions available if you search enough, but I would say the most common ones are the ones I mention here.

Adjust your application to use one of those security structures, then update your frontend to make such calls and you should be fine.

  • Related