We are facing some challenges while integrating frontend and backend server with cross domain.
Kindly have a look and assist us to resolve this issue.
Frontend (Reactjs UI.COM)
Backend (Spring Boot Okat Saml integrated Api.com) integrated with Okta SAMl Authentication
Spring security to saml authentication
Create getUser https://api.com/user Api that return user details
Created https://api.com/private controller to handle redirect
Approach##A
Frontend call fetch api Frontend/user and check if user details is present or not (Note: first user details will not present So it'll execute below steps)
2)Frontend will invoke redirect backend/private and come to backend server
3) then backend server will redirect to okta and get Saml Response
4)Now Backend server Spring Security will parse saml response and start session(i.e “BackendSession”) and prepare SamlUserDetails
5)Now time to redirect back to originater/frontend So Backend server will call redirect frontend and go back to frontend/UI and drop “BacknedSessionId” in browser cookie. (Note: Here cookie will drop aginst backend domain only)
Problem Statement: Here frontend unable to read "BackendendSessionId" which is drop aginst backend
6) Further task: need to call backend/user api to get user details and this api expecting credential i.e SamlUserDetails (i.e inside "BackendendSessionId")
Approach##B:
1 to 4 same as above .
5)Now time to redirect back to originater/frontend So Backend server will call redirect frontend and also append "BackendendSessionId" into uri/query param and go back to frontend/UI and also drop “BacknedSessionId” in browser cookie. (Note: Here cookie will drop aginst backend domain only)
6)Now frontend get "BackendendSessionId" through uri/query param
7) Now frontend call backend/user api and pass "BackendendSessionId" into request header
Problem Statement: Here Backend/user api expecting credential i.e SamlUserDetails
Note: Client is not interested in cookies at all. So we have eliminated dropping cookies approach
Code reference
https://api.com/private controller to handle redirect
#GetMapping("/private")
public ModelAndView index(#SAMLUser SAMLUserDetails user, HttpServletRequest request, HttpServletResponse response){
Map<String, String> userAttributes = user.getAttributes();
log.info("userAttributes {}", userAttributes);
log.info("saml user {}", user);
log.info("saml userName {}", user.getUsername());
String location = request.getHeader("referer");
if (isBlank(location)){
location=srtUiUrl;
}
StringBuilder sb = new StringBuilder();
sb.append(location);
sb.append("?stateId=");
sb.append(request.getSession().getId());
log.info("RedirectUrl ",sb.toString());
return new ModelAndView("redirect:" + sb.toString());
getUser https://api.com/user api return user
#GetMapping("/receive/v1/user")
public ResponseEntity<User> getUser(#SAMLUser SAMLUserDetails user) {
log.info("SAMLUserDetails : ", user);
if (null==user) {
log.info("No user found Authentication required");
return new ResponseEntity<>(null, HttpStatus.OK);
} else {
Map<String, String> userAttributes = user.getAttributes();
log.info("SAMLUserDetails Attributes {}", userAttributes);
User userModel=new User();
userModel.setId(user.getUsername());
userModel.setFirstName(userAttributes.get(SrtConstant.FIRST_NAME));
userModel.setLastName(userAttributes.get(SrtConstant.LAST_NAME));
userModel.setEmail(userAttributes.get(SrtConstant.USER_EMAIL));
userModel.setShortName(userAttributes.get(SrtConstant.STORE_NAME));
userModel.setGroupName(userAttributes.get(SrtConstant.GROUPS));
userModel.setStoreId(userAttributes.get(SrtConstant.STORE_ID));
userModel.setStoreName(userAttributes.get(SrtConstant.STORE_NAME));
log.info("userModel :", userModel);
return ResponseEntity.ok().body(userModel);
}
}
Spring security to saml authentication
#Override
protected void configure(final HttpSecurity http) throws Exception {
log.info("configure : "+"spHost :"+ spHost +" ,KeystoreFilePath : "+keyStoreFile +" ,KeyStoreAlias : "+keyStoreAlias+" ,metadatpath : "+metadataPath);
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/saml*").permitAll()
.antMatchers(HttpMethod.GET, "/receive/").permitAll()
.antMatchers(HttpMethod.POST, "/receive/").permitAll()
.antMatchers(HttpMethod.PATCH, "/receive/").permitAll()
.antMatchers(HttpMethod.DELETE, "/receive/").permitAll()
.antMatchers(HttpMethod.PUT, "/user").permitAll()
.anyRequest().authenticated()
.and()
.apply(saml())
.userDetailsService(samlUserDetailsService)
.serviceProvider()
.protocol("https")
.hostname(spHost)
.basePath("/")
.keyStore()
.storeFilePath(keyStoreFile)
.keyPassword(keyStorePassword)
.keyname(keyStoreAlias)
.and()
.and()
.identityProvider()
.metadataFilePath(metadataPath);
}
Cors filter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", srtUiUrl);
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
Same-site=none setting
server:
port: 8443
servlet:
session:
cookie:
same-site: none
secure: true
Related
I am using spring-boot-starter-parent with version 2.3.4.RELEASE and spring-security.
I have defined cors configuration as below in my spring boot app. I am able to make calls to REST API from react app running on my local without cors issue.
As part of login am hitting /user API with user credentials. Server sends user details as response and I am generating JWT in a filter only for /user API and sending JWT in Authorization header.
I have tested with postman and curl I can see the Authorization header but my react app though it can authenticate it can't see or access Authorization header or the JWT in the response of /user API.
In cors configuration I have marked Authorization header as exposed config.setExposedHeaders(Arrays.asList("Authorization")); not sure why its not getting reflected.
complete code is as below.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(new CorsConfigurationSource() {
#Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setExposedHeaders(Arrays.asList("Authorization"));
config.setMaxAge(3600L);
return config;
}
}).and()
.csrf().disable()
.addFilterBefore(new JWTTokenValidatorFilter(), BasicAuthenticationFilter.class) //JWT validation before basic authentication
.addFilterAfter(new JWTTokenGeneratorFilter(), BasicAuthenticationFilter.class) //JWT validation after basic authentication
.authorizeRequests()
.anyRequest().authenticated()//authorize all requests
.and()
.httpBasic();
}
I tried explicitly defining a cors filter but that didnt work either
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Headers", "Authorization, x-requested-with, x-auth-token");
response.addHeader("Access-Control-Expose-Headers","Authorization");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
if (!(request.getMethod().equalsIgnoreCase("OPTIONS"))) {
try {
chain.doFilter(req, res);
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
System.out.println("Pre-flight");
response.setHeader("Access-Control-Allowed-Methods", "POST, GET, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Authorization, content-type,x-auth-token, " +
"access-control-request-headers, access-control-request-method, accept, origin, authorization, x-requested-with");
response.addHeader("Access-Control-Expose-Headers","Authorization");
response.setStatus(HttpServletResponse.SC_OK);
}
}
}
I'm aware that this question has been posted several times in this or atleast some same-looking way.
But after try-and-error for the last 6 hours without any luck I had to create another. None of the answers I found solved my problem.
Setup:
Local keycloak inside docker container on port 8180
Local web frontend on port 3000
Local spring backend on port 8080
Flow:
Open web frontend => login using keycloak => request data from backend
Error:
When requesting data from backend CORS error gets fired (Only in browser, no error in spring or keycloak logs)
application.properties:
keycloak.auth-server-url=http://localhost:8180/auth
keycloak.realm=Brandsky
keycloak.resource=Brandsky-React
keycloak.ssl-required=external
keycloak.public-client=true
keycloak.cors=true
keycloak.cors-max-age=0
keycloak.principal-attribute=preferred_username
keycloak.cors-allowed-methods=POST, PUT, DELETE, GET
keycloak.cors-allowed-headers=X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers
WebSecurityConfig:
#Bean
public CorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://192.168.178.40:3000"));
configuration.setAllowedMethods(Arrays.asList(CorsConfiguration.ALL));
configuration.setAllowedHeaders(Arrays.asList(CorsConfiguration.ALL));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors().configurationSource(corsConfigurationSource())
.and()
.csrf()
.disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/admin", "/api/admin/**").hasRole(ADMIN)
.antMatchers(HttpMethod.POST, "/api/admin", "/api/admin/**").hasRole(ADMIN)
.antMatchers(HttpMethod.DELETE, "/api/admin", "/api/admin/**").hasRole(ADMIN)
.antMatchers(HttpMethod.GET, "/api", "/api/**").hasAnyRole(USER, ADMIN)
.antMatchers(HttpMethod.POST, "/api", "/api/**").hasAnyRole(USER, ADMIN)
.antMatchers(HttpMethod.DELETE, "/api", "/api/**").hasAnyRole(USER, ADMIN)
.anyRequest().permitAll();
}
Example of an endpoint:
#CrossOrigin(maxAge = 3600)
#RestController
#RequestMapping("/api/auth")
public class AuthController {
#Autowired
private UserRepository userRepository;
#GetMapping("/me")
public ResponseEntity<?> me(Authentication authentication) {
String username = authentication.getName();
User user = userRepository.findByUsername(username).get();
return ResponseEntity.ok(user);
}
}
Keycloak Realm Client config (as you see I even tried every combination of redirect uri..):
which all then results (as soon as I try to contact the backend)
Access to XMLHttpRequest at 'http://localhost:8180/auth/realms/Brandsky/protocol/openid-connect/auth?response_type=code&client_id=Brandsky-React&redirect_uri=http%3A%2F%2F192.168.178.40%3A8080%2Fsso%2Flogin&state=de0d62dd-3437-421c-a6ea-8cf26a2c9b3f&login=true&scope=openid' (redirected from 'http://192.168.178.40:8080/api/auth/me/') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
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.
We have a fully-working back-end login POST service, implemented using Spring Security, along with Spring Boot and Spring Session. A user needs to be logged-in in order to access other services. The login operation works, and so does the mechanism to restrict/allow access to the other services. This has been tested with Postman, which is "smart enough" to keep the session cookie on successive requests.
Now, we are trying to build the client on React. When using the browser's debug we can see the session cookie is sent in the response header without problems. We were trying to get the session cookie from the header and store it for successive requests, but it doesn't work. When investigating we learnt we are not meant to read the response header from the code, as explained here and here.
Our login operation should redirect to /customer/home, which works in Postman but not on our application. The behaviour we get with this is a 403 Forbidden, and the way we assess it is because the cookie is not set when redirecting, and hence the second operation (GET /customer/home) fails and returns 403. Is our understanding correct? However, the browser does not seem to keep the session cookie automatically. How are we supposed to maintain the session for subsequent requests if the cookie is not set automatically, and we are not supposed to read it manually? Are we supposed to NOT use cookies for this purpose, and issue authentication tokens instead?
We are obviously misunderstanding or missing something. Any pointers please?
Our WebSecurityConfigurerAdapter:
#EnableWebSecurity
#Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationProviderService authenticationProviderService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/customer/register").permitAll()
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.permitAll()
.defaultSuccessUrl("/customer/home", false)
.and()
.logout()
.permitAll()
.and()
.httpBasic();
http.csrf().disable();
}
//[ . . . ]
}
Our client trying to do a POST:
const mw = store => next => action => {
if(action.type == 'SUBMIT_LOGIN_USER') {
var payload = {
username: action.user.username,
password: action.user.password
};
// Build formData object.
let formData = new FormData();
formData.append('username', action.user.username);
formData.append('password', action.user.password);
return fetch('http://192.168.0.34:8080/login', {
method: 'POST',
body: formData
}).then(
r => (r)
)
.then(function(response) {
console.log(document.cookie) //empty
console.log(response.headers.get('Set-Cookie')) //null
next(action)
})
.catch(function(err) {
console.info(err);
});
} else {
next(action)
}
}
Using JWT (JSON Web Tokens) is a great way to implement security on single page applications like React.
If you're going with the JWT approach it would be efficient to use a package like axios to for http requests from the client side. Axios allows you to easily add an authorization token to all requests without hassle.
Even if you're not using JWT try using axios to send authorization tokens efficiently.
Overview
I am going to use API Gateway as the authentication which based on Spring security. I've just been following the steps in the https://spring.io/guides/tutorials/spring-security-and-angular-js/ link to create a project based on "pairs-double" module of its corresponding github project of https://github.com/spring-guides/tut-spring-security-and-angular-js.git.
Problem
The issue is the fact that when any POST request is submitted to the server the "Invalid CSRF Token" exception is thrown. An example of the thrown exception is as follows:
{
"timestamp": 1461714933215,
"status": 403,
"error": "Forbidden",
"message": "Invalid CSRF Token '1cdc44ad-43cb-44e6-b903-bec24fe903fd' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.",
"path": "/ui/test"
}
I checked an rechecked the issue but to no avail. I tested this scenario with postman and set the 'X-XSRF-TOKEN' as the header of the POST request but nothing happened.
So, as I am beginner in using Spring security approaches, I would appreciate it if anyone could suggest me a solution.
Looking at the security configuration of that project, you will notice that a XSRF-TOKEN cookie is being added in each request using a filter. So what you have to do is take the value of that cookie and store it in X-XSRF-TOKEN header. I've made a test project with similar security configuration to test out this case, the complete code looks like this:
#RestController
#SpringBootApplication
public class TestApplication extends WebSecurityConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**") // Disable authentication for all requests.
.permitAll()
.and()
.csrf().csrfTokenRepository(csrfTokenRepository())
.and()
.addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class); // Register csrf filter.
}
private Filter csrfHeaderFilter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrf != null) {
Cookie cookie = WebUtils.getCookie(request, "XSRF-TOKEN");
String token = csrf.getToken();
if (cookie == null || token != null
&& !token.equals(cookie.getValue())) {
// Token is being added to the XSRF-TOKEN cookie.
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
};
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String testGet() {
return "hello";
}
#RequestMapping(value = "/test", method = RequestMethod.POST)
public String testPost() {
return "works!";
}
}
To test this out with postman do the following:
Enable interceptor to start capturing cookies.
Perform a GET /test request and open the cookies tab. There you should notice a cookie with a name XSRF-TOKEN.
Take the value of that cookie and put it in X-XSRF-TOKEN header and perform a POST /test request.