Client-side cookie-based authentication with Spring Security - reactjs

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.

Related

Backend (Okta Saml Spring Boot) with Cross domain front end reactjs

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

CORS interfering with React application log in

I'm trying to set up a login screen on my react app. This provides authorisation to my spring boot backend to provide the data I need displayed. However, I keep getting the following error message from Chrome:
Access to fetch at 'http://localhost:8102/users' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I've tried the following and they don't seem to work:
In my React app:
Login.js
...
login = () => {
const user = {username: this.state.username, password: this.state.password};
fetch(SERVER_URL + "/login", {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
method: 'POST',
body: JSON.stringify(user)
})
.then(response => {
const jwtToken = response.headers.get("Authorization");
if(jwtToken != null) {
sessionStorage.setItem("jwt", jwtToken);
sessionStorage.setItem("username", this.state.username);
this.setState({ isAuthenticated: true });
} else {
this.setState({ open: true });
}
})
.catch(err => console.error(err));
}
...
In my App.js:
class App extends Component {
constructor(props) {
super(props);
this.state = { user: "user" };
}
componentDidMount() {
fetch(USER_URL)
.then((response) => response.json())
.then((responseData) => { this.setState({ user: responseData._embedded.user }); })
.catch((err) => console.error("THIS IS A SERIOUS ERROR INDEED!" + err));
}
render() {
return(
<div className='App'>
<Login />
</div>
)
}
}
On my Spring Boot backend:
SecurityConfiguration.java
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(new LoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
...
...
#Bean
protected CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
...
In my MainController.java1
#RestController
#CrossOrigin
public class MainController {
...
None of that seems to be properly working to get around this CORS issue.
My Server github repository is:
AssetRegister-Server
My Client github repository is: AssetRegister-Client
To make sure that spring security actually uses CORS, you have to actually enable it.
Its not enough to just register a configuration as a #Bean.
you enable CORS in you security config like this:
http.csrf().disable()
.cors()
.and()
.authorizeRequests()
Other things to point out in your code base:
#CrossOrigin can be removed since you are are defining CORS using spring security.
You have implemented a some sort of custom login solution when spring security already has something called formLogin all you need to do is enable formLogin and then send the data using javascript fetch as form data
fetch and form data and then you can remove your entire custom login. Writing custom login as you have done is bad practice, and if you would submit this code in production or to learn, you are learning very bad practices.
Spring security comes already with a JWT library built in called Nimbus, so there is no need to pull in a different one as you have done.
Using JWTs as sessions comes with security risks and there are several blogs talking about that you should not use them as sessions json-web-tokens-jwt-are-dangerous-for-user-sessions Why JWTs Suck as Session Tokens You should think about this before using JWTs as session tokens. The way they you have them implemented now you have no possibility of revoking tokens so if someone steals a token, then you cant stop them from using it maliciously until it times out.
Benefits by using formLogin is that you can use the authenticationSuccess handler to return a session/token
.formLogin(formLoginSpec -> formLoginSpec.loginPage("/login")
.authenticationFailureHandler(new AuthenticationFailureHandler())
.authenticationSuccessHandler(new AuthenticationSuccessHandler())
So what im trying to say is that if you are following a tutorial etc. Then it is very out of date, or it has no idea about what actually exists in spring security. And i recommend that you look into the official documentation. Its very good.
Try replacing #CrossOrigin with #CrossOrigin(origins = "*", allowedHeaders = "*") in MainController.java

Authenticate and access spring security protected API from a react js application

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.

CSRF/XSRF protection for Spring Security and AngularJS

I tried to add CSRF/XSRF protection to my application, but ran into strange behavior. All get requests work fine, but on all post/put/delete I'm getting 403 Unauthorized. And the strangest thing is that when I tried to debug my CSRF filter, requests do not reach it, they are rejected somewhere earlier. They do not even reach my authentication filter, so I can not figure out what the problem may be.
My security config:
#Override
public void configure(HttpSecurity http) throws Exception {
http
...
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService()), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.csrf().csrfTokenRepository(csrfTokenRepository());
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
I do not add the filters since as I said, the requests do not reach them. But if needed I will complete my question. I hope for your help, thank you in advance!
In principle, the CSRF mechanism in Spring stores the CSRF token in a HTTP only cookie. Because JavaScript cannot access a HTTP only cookie, you need to tell spring to disable HTTP only:
.and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
Then you can read the cookie from Angular and add it to the XSRF-TOKEN header with each request.
This is the general case. I am not sure if this fits your special case.
Assuming that the rest of your config/filters working properly, you're facing this issue because of this: SessionCreationPolicy.STATELESS.
You can have a look under the hood of Spring CsrfFilter. You'll see that it needs to remember the value of each CSRF-token for each user inside a session, and since you are not using sessions it can't be done.
What to do next - is really up to you. Some people saying that if you app is stateless there is actually no need for CSRF protection. Spring docs saying that CSRF attacks are still relevant. I think it really depends on your authentication mechanism.
You might also want to look at this nice article, for example.
Hope it helps.
Many thanks for the answers, they really helped me to find a solution. And I want to share my solution if in the future someone will face the same issue.
As noted in the answers I used SessionCreationPolicy.STATELESS and did not have sessions so instead of HttpSessionCsrfTokenRepository I had to use CookieCsrfTokenRepository with withHttpOnlyFalse() to allow AngularJS to read cookies.
As a result, I have a configuration like this:
#Override
public void configure(HttpSecurity http) throws Exception {
http
...
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService()), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.csrf().csrfTokenRepository(csrfTokenRepository());
}
If someone is interested in how the CsrfHeaderFilter looks:
public class CsrfHeaderFilter extends 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())) {
cookie = new Cookie("XSRF-TOKEN", token);
cookie.setPath("/");
response.addCookie(cookie);
}
}
filterChain.doFilter(request, response);
}
}
My second problem was CORS. AngularJS documentation says:
"The header will not be set for cross-domain requests."
To solve this problem, I had to use an HTTP Interceptor:
.factory('XsrfInterceptor', function ($cookies) {
return {
request: function (config) {
var headerName = 'X-XSRF-TOKEN';
var cookieName = 'XSRF-TOKEN';
config.headers[headerName] = $cookies.get(cookieName);
return config;
}
};
});
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('XsrfInterceptor');
}]);
I hope my answer will be useful.

Is it possible to login using the Spring SecurityConfiguration HttpSecurity without a password?

I know my question is weird a little bit but in an application connected to an LDAP server we need just to get the user to login into the app.
The application is a Spring AngularJS.
So now i'm changing the code to do that but i find out that the login method is done into a Spring security configuration and uses a password so i need to get the password from the Data Base (user.getPassword()), the issue is that the password is encrypted with the PasswordEncoder so there is no way to decrypte it (mentioned in an answer here in that forom) ..
anyway i wonder if there is a way to login with spring security using just the userName (without a password) ..
I tried to remove the password declaration from the configuration class but it doesn't work.
In that class called (public class SecurityConfiguration extends WebSecurityConfigurerAdapter) i had the next config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.rememberMe()
.rememberMeServices(rememberMeServices)
.rememberMeParameter("remember-me")
.key(jHipsterProperties.getSecurity().getRememberMe().getKey())
.and()
.formLogin()
.loginProcessingUrl("/api/authentication")
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.usernameParameter("j_username")
.passwordParameter("j_password")
.permitAll()
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
}
Here is the login method:
function login (credentials) {
alert("log in this");
var data = 'j_username=' + encodeURIComponent(credentials.username)
+'&j_password=' + encodeURIComponent(credentials.password) +
'&remember-me=' + credentials.rememberMe + '&submit=Login';
return $http.post('api/authentication', data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).success(function (response) {
return response;
});
}
Any idea guys?
Thank youuu
You can always create a SecurityContext at any time of your application, it doesn't matter if it has a password or not:
Authentication authentication =
new UsernamePasswordAuthenticationToken(user, null, authorities);//set with authorities
SecurityContextHolder.getContext().setAuthentication(authentication);
With this you are actually just informed to spring that you have a registered user.
I'm not sure how you're retrieving your user details, but maybe you could just use j_username for both the username and password, then just ignore it in the method that retrieves your user details.

Resources