Invalid CSRF Token in POST request - angularjs

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.

Related

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.

How to authenticate users with Social Login (Facebook, Google) in a REST API backend with Spring Boot (separate React Frontend)

I am currently working on a Spring Boot REST API. I have successfully added login using client credentials with Spring Oauth and Spring Security (I can successfully get access token and refresh token using /oauth/token endpoint).
But now I want to provide social login with Facebook and Google. As I understand, this is the flow.
User clicks Login with Social button in React frontend.
Then, he will be asked to grant access. (Still in React)
After that he will be redirected to the react front end with an access token.
Frontend sends that access token to the Spring Boot backend. (I don't know to what endpoint)
Then backend uses that access token to fetch details from the Facebook/Google and check whether a such user exists in our database.
If such user exists, backend will return access and refresh tokens to the frontend.
Now frontend can consume all the endpoints.
My problem is, I have no idea about the steps 4,5 and 6.
Do I have to make a custom endpoint to receive FB/Google access tokens?
How do I issue custom access and refresh tokens in Spring Boot?
I would really appreciate it if you could help me with this scenario.
The flow it's the following:
Front-End calls spring to /oauth2/authorization/facebook(or whatever client do you wanna use)
Back-end respond with a redirect to Facebook login page(including in the query params, client_id, scope, redirect_uri(must be the same present on your developer console) and state which is used to avoid XSRF attacks, according to OAuth2 Standards)
you can see more details here https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
state
RECOMMENDED. An opaque value used by the client to maintain
state between the request and callback. The authorization
server includes this value when redirecting the user-agent back
to the client. The parameter SHOULD be used for preventing
cross-site request forgery as described in Section 10.12.
3) Once the user log-in and accept whatever popup facebook or other services will show, the user will be redirected to the page present in "redirect_uri", this page should be a component of your ReactJs. The callback will come with some datas put in the query params, usually those params are two, state(it's the same you sent to facebook) and code(which is used from the BE to end the login flow).
Once facebook or whatever service, called you back, you have to take those 2 params, from the url(using JS for instance) and call the /login/oauth2/code/facebook/?code=CODE_GENERATED_BY_FACEBOOK&?state=STATE_GENERATED_BY_SPRING
Spring will call the facebook service(with an implementation of OAuth2AccessTokenResponseClient, using your secret_token, client_id, code and few other fields. Once facebook responds with the access_token and refresh_token, spring call an implementation of OAuth2UserService, used to get user info from facebook using the access_token created a moment before, at facebook's response a session will be created including the principal. (You can intercept the login success creating an implementation of SimpleUrlAuthenticationSuccessHandlerand adding it to your spring security configuration. (For facebook, google and otka in theory OAuth2AccessTokenResponseClient and OAuth2UserService implementations should already exist.
In that handler you can put the logic to add and look for an existing user.
coming back to the default behavior
Once spring created the new session and gave you the JSESSIONID cookie, it will redirect you to the root (I believe, I don't remember exactly which is the default path after the login, but you can change it, creating your own implementation of the handler I told you before)
Note: access_token and refresh_token will be stored in a OAuth2AuthorizedClient, stored in the ClientRegistrationRepository.
This is the end. From now then you can call your back end with that cookie and the be will see you as a logged user. My suggestion is once you got the simple flow working, you should implement a JWT token to use and store in the localstorage of your browser instead of using the cookie.
Hopefully I gave you the infos you were looking for, if I missed something, misunderstood something or something it's not clear let me know in the comment.
UPDATE (some java samples)
My OAuth2 SecurityConfig :
NOTE:
PROTECTED_URLS it's just : public static final RequestMatcher PROTECTED_URLS = new NegatedRequestMatcher(PUBLIC_URLS);
PUBLIC_URLS it's just: private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher( new AntPathRequestMatcher("/api/v1/login"));
Also note I'm using a dual HttpSecurity configuration. (But in this case it's useless to public that too)
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class OAuth2ClientSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JWTService jwtService;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
private final OAuth2AuthorizedClientService clientService;
private final UserAuthenticationService authenticationService;
private final SimpleUrlAuthenticationSuccessHandler successHandler; //This is the default one, this bean has been created in another HttpSecurity Configuration file.
private final OAuth2TokenAuthenticationProvider authenticationProvider2;
private final CustomOAuth2AuthorizedClientServiceImpl customOAuth2AuthorizedClientService;
private final TwitchOAuth2UrlAuthSuccessHandler oauth2Filter; //This is the success handler customized.
//In this bean i set the default successHandler and the current AuthManager.
#Bean("oauth2TokenAuthenticaitonFilter")
TokenAuthenticationFilter oatuh2TokenAuthenticationFilter() throws Exception {
TokenAuthenticationFilter filter = new TokenAuthenticationFilter(PROTECTED_URLS);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setAuthenticationManager(authenticationManager());
return filter;
}
#PostConstruct
public void setFilterSettings() {
oauth2Filter.setRedirectStrategy(new NoRedirectStrategy());
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider2);
}
#Bean
public RestOperations restOperations() {
return new RestTemplate();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests().antMatchers("/twitch/**").authenticated()
.and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.logout().disable().authenticationProvider(authenticationProvider2)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
.and()
.addFilterBefore(oatuh2TokenAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.oauth2Login().successHandler(oauth2Filter).tokenEndpoint()
.accessTokenResponseClient(new RestOAuth2AccessTokenResponseClient(restOperations()))
.and().authorizedClientService(customOAuth2AuthorizedClientService)
.userInfoEndpoint().userService(new RestOAuth2UserService(restOperations(), tempUserDataService, authorizedClientRepo));
}
#Bean
FilterRegistrationBean disableAutoRegistrationOAuth2Filter() throws Exception {
FilterRegistrationBean registration = new FilterRegistrationBean(oatuh2TokenAuthenticationFilter());
registration.setEnabled(false);
return registration;
}
}
By the fact that my SessionCreationPolicy.STATELESS the cookie created by spring after the end of the OAuth2 Flow is useless. So once the process its over I give to the user a TemporaryJWT Token used to access the only possible service (the register service)
My TokenAutheticationFilter:
public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String AUTHORIZATION = "Authorization";
private static final String BEARER = "Bearer";
public TokenAuthenticationFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
super(requiresAuthenticationRequestMatcher);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
String token = Optional.ofNullable(httpServletRequest.getHeader(AUTHORIZATION))
.map(v -> v.replace(BEARER, "").trim())
.orElseThrow(() -> new BadCredentialsException("Missing authentication token."));
Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
return getAuthenticationManager().authenticate(auth);
}
#Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
}
TwitchOAuth2UrlAuthSuccessHandler (This is where all the magic happens):
This handler is called once the userService and the userService is called when the user calls api.myweb.com/login/oauth2/code/facebook/?code=XXX&state=XXX. (please don't forget the state)
#Component
#RequiredArgsConstructor
public class TwitchOAuth2UrlAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final OAuth2AuthorizedClientRepo oAuth2AuthorizedClientRepo;
private final UserAuthenticationService authenticationService;
private final JWTService jwtService;
private final Gson gson;
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
response.setStatus(200);
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
Optional<OAuth2AuthorizedClientEntity> oAuth2AuthorizedClient = oAuth2AuthorizedClientRepo.findById(new OAuth2AuthorizedClientId(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
if (oAuth2AuthorizedClient.isPresent() && oAuth2AuthorizedClient.get().getUserDetails() != null) {
response.getWriter().write(gson.toJson(authenticationService.loginWithCryptedPassword(oAuth2AuthorizedClient.get().getUserDetails().getUsername(), oAuth2AuthorizedClient.get().getUserDetails().getPassword())));
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().flush();
} else {
response.setHeader("Authorization", jwtService.createTempToken(((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(), authentication.getName()));
}
}
#Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response) {
return "";
}
}
RestOAuth2AccessTokenResponseClient (its responsable to take Access_token and refresh_token from FB)
public class RestOAuth2AccessTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private final RestOperations restOperations;
public RestOAuth2AccessTokenResponseClient(RestOperations restOperations) {
this.restOperations = restOperations;
}
#Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
MultiValueMap<String, String> tokenRequest = new LinkedMultiValueMap<>();
tokenRequest.add("client_id", clientRegistration.getClientId());
tokenRequest.add("client_secret", clientRegistration.getClientSecret());
tokenRequest.add("grant_type", clientRegistration.getAuthorizationGrantType().getValue());
tokenRequest.add("code", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
tokenRequest.add("redirect_uri", authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
tokenRequest.add("scope", String.join(" ", authorizationGrantRequest.getClientRegistration().getScopes()));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
ResponseEntity<AccessResponse> responseEntity = restOperations.exchange(tokenUri, HttpMethod.POST, new HttpEntity<>(tokenRequest, headers), AccessResponse.class);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
AccessResponse accessResponse = responseEntity.getBody();
Set<String> scopes = accessResponse.getScopes().isEmpty() ?
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes() : accessResponse.getScopes();
return OAuth2AccessTokenResponse.withToken(accessResponse.getAccessToken())
.tokenType(accessResponse.getTokenType())
.expiresIn(accessResponse.getExpiresIn())
.refreshToken(accessResponse.getRefreshToken())
.scopes(scopes)
.build();
}
}
UserService
public class RestOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final RestOperations restOperations;
private final TempUserDataService tempUserDataService;
private final OAuth2AuthorizedClientRepo authorizedClientRepo;
public RestOAuth2UserService(RestOperations restOperations, TempUserDataService tempUserDataService, OAuth2AuthorizedClientRepo authorizedClientRepo) {
this.restOperations = restOperations;
this.tempUserDataService = tempUserDataService;
this.authorizedClientRepo = authorizedClientRepo;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
String userInfoUrl = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", oAuth2UserRequest.getAccessToken().getTokenValue()));
headers.add(HttpHeaders.USER_AGENT, "Discord Bot 1.0");
if (oAuth2UserRequest.getClientRegistration().getClientName().equals("OAuth2 Twitch")) {
headers.add("client-id", oAuth2UserRequest.getClientRegistration().getClientId());
}
ParameterizedTypeReference<Map<String, Object>> typeReference = new ParameterizedTypeReference<Map<String, Object>>() {
};
ResponseEntity<Map<String, Object>> responseEntity = restOperations.exchange(userInfoUrl, HttpMethod.GET, new HttpEntity<>(headers), typeReference);
if (!responseEntity.getStatusCode().equals(HttpStatus.OK) || responseEntity.getBody() == null) {
throw new SecurityException("The result of token call returned error or the body returned null.");
}
Map<String, Object> userAttributes = responseEntity.getBody();
userAttributes = LinkedHashMap.class.cast(((ArrayList) userAttributes.get("data")).get(0));
OAuth2AuthorizedClientId clientId = new OAuth2AuthorizedClientId(oAuth2UserRequest.getClientRegistration().getRegistrationId(), String.valueOf(userAttributes.get("id")));
Optional<OAuth2AuthorizedClientEntity> clientEntity = this.authorizedClientRepo.findById(clientId);
if (!clientEntity.isPresent() || clientEntity.get().getUserDetails() == null) {
TempUserData tempUserData = new TempUserData();
tempUserData.setClientId(clientId);
tempUserData.setUsername(String.valueOf(userAttributes.get("login")));
tempUserData.setEmail(String.valueOf(userAttributes.get("email")));
tempUserDataService.save(tempUserData);
}
Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
return new DefaultOAuth2User(authorities, userAttributes, oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName());
}
As asked this is all the code you need, just to give you another hint. When you call /login/oauth2/code/facebook/?code=XXX&?state=XXX the chain is the following:
RestOAuth2AccessTokenResponseClient
RestOAuth2UserService
TwitchOAuth2UrlAuthSuccessHandler
I hope this can help you. Let me know if you need more explainations.

React fetch response missing Authorization header

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.

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.

415 Unsupported Media Type with AngularJS and Spring Boot 1.4.3

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.

Resources