spring-bootauthenticationaxioscorshttp-status-code-401

Why does a 401 Unauthorized error still occur on login when CORS is configured to allow any access?


I am at a loss with the following problem after days of trying to solve it.

The context in which I am building this project is React/Axios/Spring Boot 3/Spring Security 6.

I get a 401 CORS unauthorized error when I attempt to login into the protected view of the project. These are the relevant parts:

Login.js file:

const validateUserExists = async () => { 
       
       
       
       const loginData = {
           username: state.username,
           password: state.password
       }
       
       console.log("Login Data is, ", loginData)
       
       // check username for existence
       
        try {
            console.log("Sending data...")
            const response = await userServices.getStaffByEmailAddress(loginData);
            const res = response.data
            console.log("Res data is, ", response.data)
            
            if (response.data == "Login successful!") {

                        console.log("Response is, ", resSID)
        
                // here the process will determine the role of the user 
                const respSID = await userServices.getSIDByStaffEmail(state.username)
                const resSID = respSID.data
                 
               
                    console.log("Staff id for getting email is: ", resSID)
               
                    // get rid from staff id in the staff_roles table
                    const respRID = await userServices.getRIDfromSID(resSID)
                    const resRID = respRID.data
               
                    console.log("Role id based on staff id is: ", resRID)
               
                    // get role name from the rid
                    const respRole = await userServices.getRoleByRID(resRID)
                    const role = respRole.data
               
                    console.log("Role name based on RID is: ", role) 

                                // ...ommitted code for further filtering based on role and session management

                                navigate("/ShowUsers")
                     }

                     } catch (error) {
            
            console.log("Error is, " , error)
            console.log("Error status: ", error.res.status)
            console.log("Error data: ", error.res.data)
        
        }
   }

return (
      <div className="form">
      <form onSubmit={handleSubmit} >
        
        <div className="form-row-login">
        <div className="title">Sign-In Below:</div>
        <label htmlFor="username">Username: </label>
        <input 
            type="text" 
            id="username" 
            name="username"
            placeholder="Enter your company email" 
            value={state.username} 
            onChange={(e) => setState({username: e.target.value})}
        />
          
        
        <label htmlFor="password">Password: </label>
        <input 
            type="password" 
            id="password" 
            name="password"
            placeholder="Enter your password" 
            value={state.password}
            onChange={(e) => setState({password: e.target.value})}
        />
        
        <button className="button-container" type="submit" value="Login">Log In</button>
        <button 
            className="button-container" 
            type="submit" 
            value="ForgotPassword"
            onClick={(e) => handleForgotPassword(e)}>Forgot Password?</button>
        
      </div>
      
      { isLoginPending && <div class="form-row-login">Please wait...</div> }
      { isLoggedIn && <div class="form-row-login">Success.</div> }
      { loginError && <div class="form-row-login">{loginError.message}</div> }
       
      </form>
      
      
    </div>
   );
                      

LoginData model that is used to hold the username and password:

package com.example.demo.model;

public class LoginData {
    
    private String username;
    private String password;

    
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    
    public LoginData(String username, String password) {
        super();
        this.username = username;
        this.password = password;
    }
    @Override
    public String toString() {
        return "LoginData [username=" + username + ", password=" + password + "]";
    }
    public LoginData() {
        super();
        // TODO Auto-generated constructor stub
    }
    
    

}

Axios through service.js:

         getStaffByEmailAddress(loginData) {
        
        try {
            
            console.log("LoginData at axios is, ", loginData)
            
            return axios.post('http://myappforms:8080/myapp/login/get-staff-by-email',                       loginData, {
                  headers: {
                    'Access-Control-Allow-Origin': '*',
                    'Content-Type': 'application/json; charset=UTF-8',
                    'Access-Control-Allow-Headers':'*',
                    "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,PATCH,OPTIONS"
                }})
            
        } catch (error) {
            
            console.log("Error is: ", error)
            
        }
            
    }

StaffController.java file:


@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/login")
public class StaffController {

// ... other endpoints

       @PostMapping("/get-staff-by-email")
    public ResponseEntity<String> getStaffByEmailAddress(@RequestBody LoginData loginData,                   HttpServletRequest request) {
        System.out.println(loginData.getPassword());
        System.out.println(loginData.getUsername());

        if (staffService.authenticateUser(loginData.getUsername(), loginData.getPassword())) {
            
            return ResponseEntity.ok("Login successful!");
            

        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Login failed");
        }
    }

}

StaffService.java:

Note that I ak using BCrypt here to encode the submitted password and match it against the hashed version that is already stored in the database.

public Boolean authenticateUser(String username, String password) {

               @Autowired
           StaffRepository staffRepo;
    
           @Autowired
           private PasswordEncoder passwordEncoder;
        
        Staff staff = staffRepo.findByUsername(username);
        
        System.out.println(username);
        System.out.println(password);
        System.out.println("Staff is: " + staff);
        System.out.println(staff.getUsername());
        System.out.println(staff.getStaffpass());
        
        String encryptedPass = passwordEncoder.encode(password);
        
        if (staff != null && passwordEncoder.matches(encryptedPass, staff.getStaffpass())) {
            return true;
        }
        
        return false;
    }

WebSecurityConfig file:

package com.example.demo;

import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;

//@ComponentScan(basePackages = "come.example.demo")
@Configuration (proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {
    
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    
        
        //XorCsrfTokenRequestAttributeHandler xor = new XorCsrfTokenRequestAttributeHandler();
        //String hasIpAddress = "127.0.0.1";
        
        
    return http
        .cors(cors -> cors.disable())
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement((session) ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authorizeHttpRequests(authorize -> {
            //try {
                authorize
                        .requestMatchers("/myapp/").permitAll()
                        .requestMatchers("/public/**").permitAll()
                        //.requestMatchers("/login/**").permitAll()
                        //.requestMatchers("/login/get-staff-by-email").permitAll()
                        .anyRequest().authenticated();
                       // .and()
                       // .httpBasic();
            //} catch (Exception e) {
            //  // TODO Auto-generated catch block
            //  e.printStackTrace();
            //}
        }
                
                
        )
        .httpBasic(Customizer.withDefaults())
        .build();   
        
        
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    /*
     @Bean
       
    public void addCorsMappings(CorsRegistry registry) {
        // Allow all origin to call your api
        registry.addMapping("/**");
    }
            */
    
    
      @Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("/**"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}


}

The attempted solutions I have done include the following:

  1. Dedicated CORS configuration inside the WebSecurityConfig file.
  2. Specifically configured .requestMatchers pathway permitting all requests to it.
  3. Removing the login pathways entirely from the CORS configuration
  4. @CrossOrigin annotation with unrestricted origing at the level of the controller file.
  5. Tried applying @CrossOrigin at the relevant method without effect.
  6. The LoginData model is accurate to the submitted object
  7. Using a POST and not a GET request as a point of good security practices
  8. JSON.Stringify for the loginData object in React, which I've used for submitting new clients effectively.
  9. Commented out in the configuration file is a bean for adding a CORS Mapping, but the project would not compile at that point.
  10. Ironically, CORS and CSRF are disabled in the configuration, and the browser still blocks it.
  11. Access headers are set in the Axios request to the endpoint.
  12. I've thought about password encryption being the cause, but it has to be where it is; BCrypt can, but should not be implemented in the frontend for security reasons.

CORS configuration is taken from the documentation website: See here

This is the error feedback in the browser:

Response Headers:

OPTIONS //login/get-staff-by-email HTTP/1.1
Host: myappforms:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: POST
Access-Control-Request-Headers: access-control-allow-origin,content-type
Referer: http://localhost:8080/
Origin: http://localhost:8080
Connection: keep-alive

Request Headers:

HTTP/1.1 401 
WWW-Authenticate: Basic realm="Realm"
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
WWW-Authenticate: Basic realm="Realm"
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 721
Date: Tue, 09 Jan 2024 16:29:37 GMT
Keep-Alive: timeout=20
Connection: keep-alive

Server feedback (Tomcat 10.1.17):

Here, the unsecured endpoints (e.g. locations/programs that anyone in the public can choose) work without problems, assumingly under the "/public" requestMatchers specification in the configuration file.

 No active profile set, falling back to 1 default profile: "default"
2024-01-09T11:05:43.849-05:00  INFO 3500 --- [o-8080-exec-582] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 693 ms
2024-01-09T11:05:43.871-05:00  INFO 3500 --- [o-8080-exec-582] o.s.boot.web.servlet.RegistrationBean    : Filter errorPageFilter was not registered (possibly already registered?)
2024-01-09T11:05:43.871-05:00  INFO 3500 --- [o-8080-exec-582] o.s.boot.web.servlet.RegistrationBean    : Filter springSessionRepositoryFilter was not registered (possibly already registered?)
2024-01-09T11:05:43.871-05:00  INFO 3500 --- [o-8080-exec-582] o.s.boot.web.servlet.RegistrationBean    : Filter springSecurityFilterChain was not registered (possibly already registered?)
2024-01-09T11:05:43.952-05:00  INFO 3500 --- [o-8080-exec-582] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@5f3707ba, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@78954241, org.springframework.security.web.context.SecurityContextHolderFilter@33e0b89a, org.springframework.security.web.header.HeaderWriterFilter@60141a43, org.springframework.security.web.authentication.logout.LogoutFilter@dd00125, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52e3edea, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5f71c5fc, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1f770a53, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6de51fe8, org.springframework.security.web.session.SessionManagementFilter@1a07d2c0, org.springframework.security.web.access.ExceptionTranslationFilter@27c9669c, org.springframework.security.web.access.intercept.AuthorizationFilter@7b5dd70c]
2024-01-09T11:05:44.043-05:00  INFO 3500 --- [o-8080-exec-582] com.example.demo.ServletInitializer      : Started ServletInitializer in 0.942 seconds (process running for 526142.098)
2024-01-09T11:05:56.111-05:00  INFO 3500 --- [o-8080-exec-585] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-01-09T11:05:56.191-05:00  INFO 3500 --- [o-8080-exec-585] o.s.web.servlet.DispatcherServlet        : Completed initialization in 80 ms
2024-01-09T11:05:59.281-05:00 DEBUG 3500 --- [o-8080-exec-590] org.hibernate.SQL                        : /* <criteria> */ select l1_0.lid,l1_0.lname,l1_0.lsort from locations l1_0
Hibernate: /* <criteria> */ select l1_0.lid,l1_0.lname,l1_0.lsort from locations l1_0

UPDATE

The server completes all the requests from the frontend with the encrypted passwords, but the 401 error persists.

Log has been edited for confidential information:

Hibernate: /* <criteria> */ select s1_0.sid,s1_0.resettoken,s1_0.servicecontact,s1_0.sfirstname,s1_0.slastname,s1_0.sphone,s1_0.spid,s1_0.staffpass,s1_0.tokenexpirationdate,s1_0.username from staff s1_0 where s1_0.username=?
email@myapp.com
password123
2024-01-09T14:01:07.051-05:00 DEBUG 3500 --- [o-8080-exec-620] org.hibernate.SQL                        : select r1_0.sid,r1_1.roleid,r1_1.role from staff_roles r1_0 join roles r1_1 on r1_1.roleid=r1_0.rid where r1_0.sid=?
Hibernate: select r1_0.sid,r1_1.roleid,r1_1.role from staff_roles r1_0 join roles r1_1 on r1_1.roleid=r1_0.rid where r1_0.sid=?
Staff is: Staff [SID=1, spid=1, sfirstname=MyFirstName, slastname=MyLastName, username=email@myapp.com, sphone=null, staffpass=$2a$12$..HASHEDPASSWORD123HASH, servicecontact=null, resetToken=null, tokenExpirationDate=null, roles=[Roles [roleid=1, role=USER]]]
email@,myapp.com
$2a$12$..HASHEDPASSWORD123HASH

I've gone through the relevant documentation, a number of questions on SO, AI is not of much use for this problem and I am generally new to Spring Security in general, and more so to version 6, so it is likely just down to my own lack of knowledge and experience, but I am out of ideas and directions. Any insight would be appreciated!

UPDATE 2:

It is an authentication issue in the backend, where identical hashed passwords fail to match.


Solution

  • The problem with the authentication was here, in the StaffService file:

    This is what it should say - the passwordEncoder compares the raw submitted password to the hashed version stored in the database:

    if (passwordEncoder.matches(password, staff.getStaffpass())) {
        return true;
    }
        
    

    And this is what the server now reports:

    Submitted password is password123
    BCrypt password in database is: $2a$10$zJMb0woA4R.rJgvevFMViuBxEyDGNk01XpS25smM6.57cMYx9.KFK
    Returned true!
    

    Previously:

    String encryptedPass = passwordEncoder.encode(password);
        
        System.out.println("Submitted password is " + password);
        System.out.println("Encrypted passwords is " + encryptedPass);
        System.out.println("BCrypt password in database is: " + staff.getStaffpass());
        
        
        if (passwordEncoder.matches(encryptedPass, staff.getStaffpass())) {
            return true;
        }
    

    BCrypt would create a new hash with the same submitted password, which then the passwordEncoder takes and compares to the one stored in the database - and naturally, they will not match.

    The result was visible in the server log:

    Submitted password is password123
    Encrypted passwords is $2a$10$AGB9.oEy6NUvNDkka3.FK.jPpOQ/Hwy09/1cwaESfx07o1giYzhzq
    BCrypt password in database is: $2a$10$zJMb0woA4R.rJgvevFMViuBxEyDGNk01XpS25smM6.57cMYx9.KFK
    Return is false!
    

    Cue 401 unauthorized error.