I have some issues with CORS using React fetch and Spring Boot.
The React application runs on localhost:3000.
The Spring Boot backend runs on localhost:3001.
My problem is when I try to logging in using using fetch with the http://localhost:3001/login url the response in javascript does not contain the Authorization token.
The authentication on backend side works.
When I open the Chrome Inspector I can see the Authorization in the Network tab at the login request only it is missing in the javascript response.
The React fetch request look like the following: In the code the const userToken = response.headers.get('Authorization'); returns "null" string instead of the token.
return fetch("http://localhost:3001/login",{
method: 'post',
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
password
})
})
.then(
response => {
if(response.ok) {
const userToken = response.headers.get('Authorization');
return true;
}
// Error handling
}
);
The Spring Boot Security config is like the following:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable().authorizeRequests()
.antMatchers(HttpMethod.POST, REGISTRATION_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(// Auth Filter)
.addFilter(// Another auth Filter)
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
Another thing. When I used proxy: "http://127.0.0.1:3001" in the package.json the login worked and the React code above could read Authorization header. But I don't want to use proxy.
Related
In my project when logging a user in a jwt token gets returned in the response header but I cannot access this header, can anybody tell me how ?
this is my login function
const username = useRef();
const password = useRef();
const navigate = useNavigate();
function loginHandler() {
axios.post("/auth/login", {
username: username.current.value,
password: password.current.value
})
.then((response) => {
console.log(response.headers)
window.localStorage.setItem("token", response.headers.get("Authorization"))
navigate("/shoes/mens")
});
}
here is the Authorization header of my response so you can see that it does indeed return a jwt token
Security configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http = http.csrf().disable().cors().and();
http = http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http = http.exceptionHandling()
.authenticationEntryPoint((request, response, ex) -> {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage());
}).and();
http.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/storage/**").hasAuthority("ROLE_EMPLOYEE")
.anyRequest().authenticated();
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
This used to work for me but now when enabline cors.and() in backend it stopped working for me. thanks in advance for you help!
I managed to find out how to fix it after hours of searching, for someone with the same problem
create a configuration file that implements WebMvcconfigurer
and overide the addCorsMappings method then in the exposed headers u put the headers u want to expose eg Authorization `
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.exposedHeaders("Authorization");
}
}
My objective is to access spring security protected API from a react js application after authentication.
Spring boot application hosted at http://myserver:8080
React JS application is hosted at http://myserver:3000
I am able to authenticate and access the APIs using curl as follows:
Access login url with credentials . Extract jsessionid token from response header.
Access end url with jsessionid token.
$ curl -i -X POST login_url --data 'username=myusername&password=mypassword'
$ curl end_url -H 'Cookie: JSESSIONID=session_token'
I am trying to replicate the same through React JS Application.
Even though JSESSIONID Cookie is present in response header (verified through curl , and browser dev tools) but axios response header is not able to capture it.
I understand that "Set-Cookie" header in JavaScript code will not work by default. As discussed in this question React Axios, Can't read the Set-Cookie header on axios response
Kindly help with modification required in code to achieve the same. OR suggest alternate way to achieve the objective.
Thanks.
Client side code is as follows:
const onSubmitAuthenticateButton = (e) => {
e.preventDefault();
const loginUrl = 'http://myserver:8080/login';
axios.defaults.withCredentials = true;
axios.post(loginUrl, { username, password})
.then(res => console.log(res.headers))
.catch(err => console.log(err.message));
}
In Spring Secuirty configuration, csrf is disabled and cors allowed origin for "http://myserver:3000".
WebSecurityConfig class
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private CustomAuthenticationProvider customAuthProvider;
public WebSecurityConfig(CustomAuthenticationProvider customAuthProvider) {
super();
this.customAuthProvider = customAuthProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().fullyAuthenticated()
.and()
.formLogin();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthProvider);
}
}
WebMvcConfig class
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://myserver:3000")
.allowedMethods("GET", "POST")
.exposedHeaders("Set-Cookie")
.maxAge(MAX_AGE_SECS)
.allowCredentials(true);
}
}
I have achieved the objective through alternate way.
Instead of session based authentication, i am now using stateless authentication. Upon successful authentication a jwt token is returned as response. Subsequent API call, the jwt token is attached as payload. The application checks for the validity of token before processing the API call request.
I have a ReactJS and Java Spring Boot applications, both secured by Keycloak 11.0.2.
Keycloak is on port 8083, ReactJS on 3000 and Spring App is on 8085.
If I try to use the configuration provided below, I'm not able to hit my endpoint and I'm getting CORS error.
Firefox:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8083/auth/realms/sorcerer_realm/protocol/openid-connect/auth?response_type=code&client_id=event_sorcerer&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2Fsso%2Flogin&state=f52216b1-c235-4328-a2f9-d8448c3bf886&login=true&scope=openid. (Reason: CORS request did not succeed).
Chrome and Microsoft Edge:
Access to XMLHttpRequest at 'http://localhost:8083/auth/realms/sorcerer_realm/protocol/openid-connect/auth?response_type=code&client_id=event_sorcerer&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2Fsso%2Flogin&state=f57ffa9f-9679-4476-aa03-af86c3abb3c2&login=true&scope=openid' (redirected from 'http://localhost:8085/api/worker/create/product') from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
xhr.js:184 GET http://localhost:8083/auth/realms/sorcerer_realm/protocol/openid-connect/auth?response_type=code&client_id=event_sorcerer&redirect_uri=http%3A%2F%2Flocalhost%3A8085%2Fsso%2Flogin&state=f57ffa9f-9679-4476-aa03-af86c3abb3c2&login=true&scope=openid net::ERR_FAILED
When I try to hit my endpoint using Postman, I'm able to hit it. Below is my Keycloak Web Security configuration. The configuration uses application.properties file to configure Keycloak adapter.
When I set .authorizeRequests().antMatchers("/**").permitAll() in the config, I'm also able to hit my endpoint from browser and Postman.
#KeycloakConfiguration
public class SecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder authBuilder) throws Exception {
final KeycloakAuthenticationProvider authProvider = keycloakAuthenticationProvider();
authProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
authBuilder.authenticationProvider(authProvider);
}
/**
* Call superclass configure method and set the Keycloak configuration
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.cors()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().anonymous()
//.and().authorizeRequests().antMatchers("/**").permitAll() //Uncomment for requests to be allowed!
.and().authorizeRequests().antMatchers("/api/admin/**").hasRole("ADMIN")
.and().authorizeRequests().antMatchers("/api/manager/**").hasAnyRole("MANAGER")
.and().authorizeRequests().antMatchers("/api/worker/**").hasRole("WORKER")
.anyRequest().authenticated();
}
/**
* Setup Auth Strategy. Don't add prefixes and suffixes to role strings
*/
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
/**
* Don't use keycloak.json. Instead, use application.yml properties.
* #return
*/
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
Here is a part of application.properties that sets up Keycloak:
spring:
jersey:
type: filter
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8083/auth/realms/sorcerer_realm/protocol/openid-connect/token
jwk-set-uri: http://localhost:8083/auth/realms/sorcerer_realm/protocol/openid-connect/certs
keycloak:
realm: sorcerer_realm
auth-server-url: http://localhost:8083/auth/
ssl-required: external
resource: event_sorcerer
verify-token-audience: true
credentials:
secret-jwt:
secret: d84611c9-af79-423b-b12c-bfa7fec23e85
use-resource-role-mappings: true
confidential-port: 0
Here is my ReactJS application's Keycloak adapter setup:
const keycloakConfig = {
"clientId": "event_sorcerer_frontend",
"realm": "sorcerer_realm",
"auth-server-url": "http://localhost:8083/auth/",
"url": "http://localhost:8083/auth",
"ssl-required": "external",
"resource": "event_sorcerer",
"public-client": true,
"verify-token-audience": true,
"use-resource-role-mappings": true,
"confidential-port": 0
};
const keycloak = new Keycloak(keycloakConfig);
const initKeycloak = (onSuccessCallback, onFailureCallback) => {
let success = false;
timeoutWrapper(() => {
if(!success){
onFailureCallback();
}
});
keycloak.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
pkceMethod: 'S256',
}).then((isAuthenticated) => {
success = true;
if(isAuthenticated) {
onSuccessCallback();
} else {
login();
}
});
}
Here is how I send the request to server:
export const Request = {
configureAxiosDefault: () => {
axios.defaults.baseURL = axiosDefaultConfiguration.baseUrl;
},
create: (data, endpoint, callback, errorCallback, finalCallback) => {
axios.post(serverEndpoint + endpoint, {
data: data,
headers: {
Authorization: `Bearer ${UserService.getToken()}`
}
})
.then(response => Utility.isEmpty(callback) ? defaultCallback(response) : callback(response))
.catch(response => Utility.isEmpty(errorCallback) ? defaultErrorCallback(response) : errorCallback(response))
.finally(response => {
if(!Utility.isEmpty(finalCallback)) {
finalCallback(response);
}
});
},
}
Here is my Keycloak configuration for frontend. Backend is the same, except the Access Type is confidential and the Root/Base url are different (not 3000 but 8085):
Here is my CORS configuration bean:
#Configuration
public class CORSConfiguration {
/**
* Setup CORS
* #return
*/
#Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
config.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL));
config.setAllowedHeaders(Arrays.asList(CorsConfiguration.ALL));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
return source;
}
}
And lastly, here is my endpoint. URL resolves to api/worker/create/product
#RestController
#RequestMapping(ControllerEndpointsPrefix.WORKER + "/create")
public class CreationController {
#Autowired
private UserAgregate userAgregate;
#PostMapping("/product")
public boolean createProduct(#RequestBody CreateProductCommand command) {
return true;
}
}
I've managed to solve this.
The problem wasn't on the server side, but on client side.
configureAxiosDefault: () => {
axios.defaults.baseURL = axiosDefaultConfiguration.baseUrl;
axios.defaults.headers.Authorization = `Bearer ${UserService.getToken()}`
},
create: (data, endpoint, callback, errorCallback, finalCallback) => {
axios.post(serverEndpoint + endpoint, data)
.then(response => Utility.isEmpty(callback) ? defaultCallback(response) : callback(response))
.catch(response => Utility.isEmpty(errorCallback) ? defaultErrorCallback(response) : errorCallback(response))
.finally(response => {
if(!Utility.isEmpty(finalCallback)) {
finalCallback(response);
}
});
},
Server was unable to process the token, because I was sending it as a JSON object property. These changes made everything work OK.
So, CORS wasn't an issue at all. The issue was that request didn't contain an Authorization header.
There are a lot of StackOverflow questions regarding KeyCloak, and some of them incomplete and cryptic. I encountered a good amount of errors, because of OpenJDK, JDK versions etc.
If anyone needs explanations and solutions, working Spring Boot configuration is on my repository:
https://github.com/milosrs/EventSorcererBackend
Context
Spring Boot
React Js
Issue
I want to make a login request from react to get the jsessionid. I am getting a weird response from Spring Boot. In the response i don't find any cookies. In postman i can just give the username and password in the url as parameters and in the response I am getting a response with the cookie jsessionid and for more requests I can just use it. But in react I am getting a weird response and I don't know how to get the cookie.
Here is the code that sends the request from React JS to Spring Boot:
const { username, password } = this.state;
const student = { username, password };
fetch("http://localhost:8080/login", {
method: "POST",
body: new URLSearchParams(student)
})
.then(res => {
console.log(res);
const jsessionid = document.cookie;
console.log("id", jsessionid);
//Here I am trying to get the jsessionid
})
.catch(error => console.log(error));
This is the response that I am getting and that I printed out in the console
And here is my Spring Securtiy Configuration Class:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Autowired
UserDetailsServiceImpl userDetailsService;
#Bean
DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
return daoAuthenticationProvider;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().cors().and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Here I tried with curl and as you can see I am getting the cookie jsessionid .
You can access your jsessionid from response.header
for example
fetch("http://localhost:8080/login", {
method: "POST",
body: new URLSearchParams(student)
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
})
.then(res=>console.log(res.headers.get('set-cookie'));)
.catch(error => console.log(error));
When I am trying to authenticate an user from AngularJS, I am seeing this warning in Spring Boot log:
[WARN ] 2017-02-04 17:09:20.085 [http-nio-8080-exec-1] DefaultHandlerExceptionResolver - Resolved exception caused by Handler execution: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'null' not supported
And the browser response is:
415 Unsupported Media Type
My LoginController:
#RestController
// #RequestMapping("/")
public class LoginController {
public Logger logger = LoggerFactory.getLogger(this.getClass());
#RequestMapping(value = "/login", method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_VALUE
/*produces = MediaType.APPLICATION_JSON_VALUE*/)
public ResponseEntity<Admin> login(#RequestBody UserDTO user, BindingResult result, WebRequest request) {
logger.info("********** Inside login of LoginController **************");
Admin authenticatedUser = (Admin) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
HttpStatus httpStatus = null;
if (authenticatedUser == null) {
httpStatus = HttpStatus.NOT_FOUND;
} else {
httpStatus = HttpStatus.OK;
}
return new ResponseEntity<Admin>(authenticatedUser, httpStatus);
}
}
My AngularJS code:
service.login = function(user, successHandler, errorHandler) {
// Obtain a CSRF token
loginResources.options().$promise.then(function (response) {
console.log('Obtained a CSRF token in a cookie', response);
// Extract the CSRF token
var csrfToken = Cookies.getFromDocument($http.defaults.xsrfCookieName);
console.log('Extracted the CSRF token from the cookie', csrfToken);
// Prepare the headers
var headers = {
'Content-Type': 'application/json'
};
headers[$http.defaults.xsrfHeaderName] = csrfToken;
console.log("Before calling /login, user : ", user);
// Post the credentials for logging in
$http.get(ApiBasePath + '/login', user, {headers: headers})
.success(successHandler)
.error(function (data, status, headers, config) {
if (isCSRFTokenInvalidOrMissing(data, status)) {
console.error('The obtained CSRF token was either missing or invalid. Have you turned on your cookies?');
} else {
// Nope, the error is due to something else. Run the error handler...
errorHandler(data, status, headers, config);
}
});
}).catch(function(response) {
console.error('Could not contact the server... is it online? Are we?', response);
});
};//login function ends
I have an exactly same registration controller with an exactly same AngularJS register function (with different endpoint of course), but that works perfectly.
I doubt one thing though, when I am using Spring Security, do I really need the LoginController with the endpoint /login or the security configuration will take care of that? My security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/*/**").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/register").permitAll()
.antMatchers("/", "/**/*.css", "/**/**/*,css",
"/**/*.js", "/**/**/*.js").permitAll()
.antMatchers("/dashboard", "/dasboard/**", "/logout").authenticated();
// Handlers and entry points
http
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http
.formLogin()
.successHandler(authenticationSuccessHandler);
http
.formLogin()
.failureHandler(authenticationFailureHandler);
// Logout
http
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler);
// CORS
http
.addFilterBefore(corsFilter, ChannelProcessingFilter.class);
// CSRF
http
.csrf().requireCsrfProtectionMatcher(
new AndRequestMatcher(
// Apply CSRF protection to all paths that do NOT match the ones below
new NegatedRequestMatcher(new AntPathRequestMatcher("/", HttpMethod.OPTIONS.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/", HttpMethod.GET.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/", HttpMethod.POST.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/", HttpMethod.HEAD.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/", HttpMethod.TRACE.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/css/**", HttpMethod.GET.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/js/**", HttpMethod.GET.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/js/**/**", HttpMethod.GET.toString())),
// We disable CSRF at login/logout, but only for OPTIONS methods
new NegatedRequestMatcher(new AntPathRequestMatcher("/login*/**", HttpMethod.OPTIONS.toString())),
new NegatedRequestMatcher(new AntPathRequestMatcher("/logout*/**", HttpMethod.OPTIONS.toString())),
//Disable CSRF at register for all methods
new NegatedRequestMatcher(new AntPathRequestMatcher("/register*/**", HttpMethod.OPTIONS.toString()))
)
);
http
.addFilterAfter(new CsrfTokenResponseCookieBindingFilter(), CsrfFilter.class); // CSRF tokens handling
}
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authProvider());
}
#Bean
public DaoAuthenticationProvider authProvider() {
final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(encoder());
return authProvider;
}
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
Finally I got the answer. It is true, if I try to send json object instead of request parameters, that I have to use Custom UserNamePasswordAuthenticationFilter. It is also true, I have to use POST.
Thanks #dur for pointing that.
Finally, a big thanks to this post. Without this post, I wouldn't have possibly find out how to customize the filter.