Flutter post request not returning with Spring boot server login

Issue

I’m writing a Flutter web project with a Spring boot backend and am really battling with getting the authentication stuff to work.

In flutter web I have a "sign_in" method which receives an email and password and passes it to a repository method which sends a post request to the server. See code below. Currently it appears as if the post never returns as the "done with post" line never prints.

 Future<String> signIn(String email, String password) async {
    authenticationRepository.setStatus(AuthenticationStatus.unknown());

    print('signIn user: email: $email pw: $password');
    User user = User('null', email, password: password);
    //print('user: $user');
    var url;
    if (ServerRepository.SERVER_USE_HTTPS) {
      url = new Uri.https(ServerRepository.SERVER_ADDRESS,
          ServerRepository.SERVER_AUTH_LOGIN_ENDPOINT);
    } else {
      url = new Uri.http(ServerRepository.SERVER_ADDRESS,
          ServerRepository.SERVER_AUTH_LOGIN_ENDPOINT);
    }
    //  print('url: $url');

    var json = user.toUserRegisterEntity().toJson();

      print('Sending request: $json');

   // var response = await http.post(url, body: json);
    var response = await ServerRepository.performPostRequest(url,  jsonBody: json, printOutput: true, omitHeaders: true );

      print('Response status: ${response.statusCode}');
     print('Response body b4 decoding: ${response.body}');
    Map<String, dynamic> responseBody = jsonDecode(response.body);
     print('Response body parsed: $responseBody');

    if (response.statusCode != 201) {
      authenticationRepository
          .setStatus(AuthenticationStatus.unauthenticated());
      throw FailedRequestError('${responseBody['message']}');
    }

    User user2 = User(
        responseBody['data']['_id'], responseBody['data']['email'],
        accessToken: responseBody['accessToken'],
    refreshToken: responseBody['refreshToken']);

    print('user2 $user2');

    authenticationRepository
        .setStatus(AuthenticationStatus.authenticated(user2));

    return responseBody['data']['_id']; // return the id of the response

  }



static Future<Response> performPostRequest(Uri url, {String? accessToken, var jsonBody, bool printOutput = false, bool omitHeaders=false} ) async {

   var body = json.encode(jsonBody ?? '');

   if(printOutput){
     print('Posting to url: $url');
     print('Request Body: $body');
   }
   Map<String, String> userHeader = {
     HttpHeaders.authorizationHeader: 'Bearer ${accessToken ?? 'accessToken'}',
     "Content-type": "application/json",
     };
   if(omitHeaders){
     userHeader = { };
   }

   print('performing post: ');
   var response =  await http.post(
      url,
      body: body,
      headers: userHeader,
    );
   print('done with post?!');

    if(printOutput){
      print('Response status: ${response.statusCode}');
      print('Response body: ${response.body}');
      Map<String, dynamic> responseBody = jsonDecode(response.body);
      print('Response body parsed: $responseBody');
    }
    return response;
  }

My console output is as follows when attempting the request:

signIn user: email: XXXXXX@gmail.com pw: XXxxXXx500!
Sending request: {email: XXXXXX@gmail.com, password: XXxxXXx500!}
Posting to url: http://localhost:8080/auth/login
Request Body: {"email":"XXXXXX@gmail.com","password":"XXxxXXx500!"}
performing post: 

So it seems like the response is never sent by the server.

On my server, using Spring boot security the setup is as follows (I based it from this tutorial). Securityconfig:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final JWTUtils jwtTokenUtil;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   
  auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(jwtTokenUtil, authenticationManagerBean());
        customAuthenticationFilter.setFilterProcessesUrl("/auth/login");

        http.csrf().disable();
        //http.cors(); //tried but still no repsonse
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authorizeRequests().antMatchers( "/auth/**").permitAll(); // no restrictions on this end point


        http.authorizeRequests().antMatchers(POST, "/users").permitAll();
        http.authorizeRequests().antMatchers(GET, "/users/**").hasAnyAuthority("ROLE_USER");
        http.authorizeRequests().antMatchers(POST, "/users/role/**").hasAnyAuthority("ROLE_ADMIN");

        http.authorizeRequests().anyRequest().authenticated();
      
        http.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

And the filter handling the "/auth/login" end point:

@Slf4j
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {


    private final JWTUtils jwtTokenUtil;

    private final AuthenticationManager authenticationManager;

    @Autowired
    public CustomAuthenticationFilter(JWTUtils jwtTokenUtil, AuthenticationManager authenticationManager) {
        this.jwtTokenUtil = jwtTokenUtil;

        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("attemptAuthentication");

        log.info("type "+request.getHeader("Content-Type"));

        try {
//Wrap the request
        MutableHttpServletRequest wrapper = new MutableHttpServletRequest(request);

//Get the input stream from the wrapper and convert it into byte array
        byte[] body;

            body = StreamUtils.copyToByteArray(wrapper.getInputStream());
            Map<String, String> jsonRequest = new ObjectMapper().readValue(body, Map.class);

            log.info("jsonRequest "+jsonRequest);

            String email = jsonRequest.get("email");
            String password = jsonRequest.get("password");
            log.info("jsonRequest username is "+email);
            log.info("jsonRequest password is "+password);

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);

            return authenticationManager.authenticate(authenticationToken);

        } catch (IOException e) {
            e.printStackTrace();
        }

        //if data is not passed as json, but rather form Data - then this should allow it to work as well
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        log.info("old username is "+email);
        log.info("old password is "+password);

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(email, password);

        return authenticationManager.authenticate(authenticationToken);

    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("successfulAuthentication");

       User user = (User) authResult.getPrincipal();

        String[] tokens = jwtTokenUtil.generateJWTTokens(user.getUsername()
                ,user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList())
                , request.getRequestURL().toString() );

        String access_token = tokens[0];

        String refresh_token = tokens[1];
        log.info("tokens generated");

        Map<String, String> tokensMap = new HashMap<>();

        tokensMap.put("access_token", access_token);
        tokensMap.put("refresh_token", refresh_token);
        response.setContentType(APPLICATION_JSON_VALUE);

        log.info("writing result");
        response.setStatus(HttpServletResponse.SC_OK);
        new ObjectMapper().writeValue(response.getWriter(), tokensMap);

    }
}

When I try the "auth/login" endpoint using postman, I get the correct response with the jwt tokens. See below:

Postman result

I’m really stuck and have no idea how to fix it. I’ve tried setting cors on, changing the content-type (which helped making the server see the POST request instead of an OPTIONS request). Any help/explanation would be greatly appreciated.

Solution

After lots of trial and error I stumbled across this answer on a JavaScript/ajax question.

It boils down to edge/chrome not liking the use of localhost in a domain. so, if you’re using a Spring Boot server, add the following bean to your application class (remember to update the port number):

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:56222"));
    corsConfiguration.setAllowedHeaders(Arrays.asList("Origin","Access-Control-Allow-Origin",
            "Content-Type","Accept","Authorization","Origin,Accept","X-Requested-With",
            "Access-Control-Request-Method","Access-Control-Request-Headers"));
    corsConfiguration.setExposedHeaders(Arrays.asList("Origin","Content-Type","Accept","Authorization",
            "Access-Control-Allow-Origin","Access-Control-Allow-Origin","Access-Control-Allow-Credentials"));
    corsConfiguration.setAllowedMethods(Arrays.asList("GET","PUT","POST","DELETE","OPTIONS"));
    UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
    return new CorsFilter(urlBasedCorsConfigurationSource);

}

Answered By – TM00

Answer Checked By – Jay B. (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *