I'm facing a CORS issue which I'm almost sure is on the React client side.
My process is:
- A React client executes a POST fetch to my Spring Boot server (hosted in a VPS) [fetch 1]
- The request is correctly accepted by my Spring Boot server.
- It generates a token (just a custom dummy random) which will be stored in a @SessionScope object
- The Spring Boot server response is getting to my React app, with the generated dummy token
- My React App ensures that the token is there (not null in the json response) and use it to immediately make another request to my Spring Boot server through fetch [fetch 2]
I'm running my React App in my local machine on port http://localhost:3000. And my Spring Boot server is on a VPS http://vpsip:port
When I do all that process manually through Postman, the responses I get from the Spring Boot VPS server include the JSESSIONID and allow me to chain multiple requests (in which I use my dummy token) and it works perfectly.
However when I do it through the browser (React App) no JSESSIONID is coming. The only thing that works is running my Spring Boot project locally and my React App locally (both of them on same host) and only then I can see the JSESSIONID in the browser Application->Cookies
But once I aim my React fetch requests to the VPS IP (which is the same Spring Boot project) the requests go through but. No JSESSIONID is coming in the response and, a new session is created for each request that comes out of my React App, that of course affects the dummy token life cycle.
This is my CORS config for the Spring Boot project:
@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("PUT", "DELETE", "POST", "GET")
.allowedHeaders("CustomAuth", "Authorization", "header3", "Origin", "Access-Control-Allow-Origin", "Content-Type",
"Accept", "Origin, Accept", "X-Requested-With",
"Access-Control-Request-Method", "Access-Control-Request-Headers")
.exposedHeaders("CustomAuth", "Origin", "Content-Type", "Accept", "Authorization",
"Access-Control-Allow-Origin", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials")
.allowCredentials(true);
}
}
This is the React app method that triggers the chain of fetch requests:
const callUseValidateLogin = async () => {
const response1 = JSON.parse(await validateLogin(username, password));
const {sessionToken, error} = response1;
if (error == null) {
localStorage.setItem('sessionToken', sessionToken);
const {availableMonths, error} = JSON.parse(await fetchMonths());
setResponse({availableMonths});
if(error == null) {
// Get the token from localStorage
navigate("/months");
}
}
};
and [fetch 1] method (first await)
export const validateLogin = async (userName, password) => {
const url = 'http://vps.ip:port/login';
const loginData = {
username: userName,
password: password,
};
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(loginData),
});
if (response.ok) {
return await response.text();
} else {
throw new Error(`Request failed with status ${response.status}`);
}
}
and [fetch 2] method (second await)
export const fetchMonths = async () => {
const sessionToken = localStorage.getItem('sessionToken');
console.log('retrieving months with: ' + sessionToken)
const url = 'http://vps.ip:port/months';
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'CustomAuth': sessionToken,
},
});
if (response.ok) {
return await response.text();
} else {
throw new Error(`Request failed with status ${response.status}`);
}
};
I'm using credentials: 'include',. Which is one of the most common answers I have found in the community. But I don't know why it is not working for me.
In Node.js you just have to use something like app.use(cors()) and that's it, But as I mentioned, I think the problem is in my React App, since all my communication process works if you manually simulate it through Postman. I can't find out what I'm doing wrong.
Current code after keeping trying for Cors Config:
package com.example.churchbillboard2.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhostPublicIp:3000", "http://friendsLaptopPublicIP:3000","http://localhost:3000")
.allowedMethods("PUT", "DELETE", "POST", "GET", "OPTIONS")
.allowedHeaders("Accept", "CustomAuth", "Authorization", "Access-Control-Allow-Credentials", "Content-Type", "credentials", "Origin", "Access-Control-Allow-Origin")
.allowCredentials(true);
}
}
The Controller is managing all of my server requests (I'm just starting the project):
@RestController
@RequestMapping("/")
public class Login {
private UserService userService;
private TimeManager timeManager;
private SessionTokenWrapper sessionTokenWrapper;
private FamilyEventService familyEventService;
public Login(UserService userService, TimeManager timeManager,
SessionTokenWrapper sessionTokenWrapper, FamilyEventService familyEventService) {
this.userService = userService;
this.timeManager = timeManager;
this.sessionTokenWrapper = sessionTokenWrapper;
this.familyEventService = familyEventService;
}
@PostMapping(value = "/login")
public SessionToken getMethodName(@RequestBody LoginDTO user, HttpServletRequest request, HttpSession session) {
String origin = request.getHeader("Origin");
System.out.println("Origin: " + origin);
SessionToken sessionToken = (userService.getUserByUserName(user) == null) ? new SessionToken("Invalid User")
: new SessionToken(null);
sessionTokenWrapper.setSessionToken(sessionToken.getSessionToken());
System.out.println(sessionToken.getSessionToken());
return sessionToken;
}
@PostMapping("/months")
public AvailableMonthsWrapper getMethodName(@RequestHeader("CustomAuth") String headerValue,
HttpSession session) {
System.out.println(sessionTokenWrapper.getSessionToken());
System.out.println(headerValue);
return (sessionTokenWrapper.validateToken(headerValue))
? new AvailableMonthsWrapper(timeManager.availableMonths())
: new AvailableMonthsWrapper("Not Valid Session");
}
@PostMapping(value="/monthData")
public MonthFamilyEventsWrapper postMethodName(@RequestBody String month) {
return familyEventService.getFamilyEventsByDateWithEventType(month);
}
My @SessionScope class (SessionTokenWrapper):
package com.example.churchbillboard2.controllers;
@Component
@SessionScope
public class SessionTokenWrapper implements Serializable{
private String sessionToken;
public SessionTokenWrapper(String sessionToken) {
this.sessionToken = sessionToken;
}
public SessionTokenWrapper() {
}
public String getSessionToken() {
return sessionToken;
}
public void setSessionToken(String sessionToken) {
this.sessionToken = sessionToken;
}
public boolean validateToken(String in_token) {
return sessionToken != null && in_token.equals(sessionToken);
}
@PostConstruct
public void postc() {
System.out.println("SessionTokenWrapper: I was created");
}
@PreDestroy
public void preD() {
System.out.println("SessionTokenWrapper: I was deleted");
}
}
Note: I'm not using any Spring Security library, I just would like to make it work as this simple.