Reputation: 435
I have edited the question to make more sense. The original was:
How can I fix Angular and Spring Boot configuration with Spring JDBC authentication so that I am able to logout even with CSRF protection enabled?
With CSRF disabled for /logout:
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Postman.
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Firefox and the Angular frontend.
With CSRF enabled for /logout:
I am able to login (receiving CSRF and JSESSIONID cookies) and logout (200 OK is received) using Postman.
I am able to login using Firefox and the Angular frontend.
When trying to log out, however...
There is first a preflight request that succeeds:
Then, I see a request to /logout:
I debugged the backend and it seems Spring is unable to find a matching CSRF token in its TokenRepository. So I end up with a MissingCsrfTokenException and a 403 Forbidden. How can I fix that?
Backend:
SecurityConfiguration:
package org.adventure.configuration;
import org.adventure.security.RESTAuthenticationEntryPoint;
import org.adventure.security.RESTAuthenticationFailureHandler;
import org.adventure.security.RESTAuthenticationSuccessHandler;
import org.adventure.security.RESTLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private RESTLogoutSuccessHandler restLogoutSuccessHandler;
@Autowired
private DataSource dataSource;
@Autowired
private AccountsProperties accountsProperties;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().antMatchers("/h2-console/**")
.permitAll();
httpSecurity.authorizeRequests().antMatchers("/secure/**").authenticated();
httpSecurity.cors().configurationSource(corsConfigurationSource());
httpSecurity.csrf()
.ignoringAntMatchers("/h2-console/**")
.ignoringAntMatchers("/login")
//.ignoringAntMatchers("/logout")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
httpSecurity.headers()
.frameOptions()
.sameOrigin();
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
httpSecurity.formLogin().successHandler(authenticationSuccessHandler);
httpSecurity.formLogin().failureHandler(authenticationFailureHandler);
httpSecurity.logout().logoutSuccessHandler(restLogoutSuccessHandler);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
JdbcUserDetailsManagerConfigurer jdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication()
.dataSource(dataSource)
.withDefaultSchema();
if (Objects.nonNull(accountsProperties)) {
FirstUser firstUser = accountsProperties.getFirstUser();
if (Objects.nonNull(firstUser)) {
String name = firstUser.getName();
String password = firstUser.getPassword();
if (Objects.nonNull(name) && Objects.nonNull(password) &&
!("".equals(name) || "".equals(password))) {
jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
.password(passwordEncoder().encode(password))
.roles("USER"));
}
}
FirstAdmin firstAdmin = accountsProperties.getFirstAdmin();
if (Objects.nonNull(firstAdmin)) {
String name = firstAdmin.getName();
String password = firstAdmin.getPassword();
if (Objects.nonNull(name) && Objects.nonNull(password) &&
!("".equals(name) || "".equals(password))) {
jdbcUserDetailsManagerConfigurer.withUser(User.withUsername(name)
.password(passwordEncoder().encode(password))
.roles("ADMIN"));
}
}
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Strength increased as per OWASP Password Storage Cheat Sheet
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowedHeaders(List.of("X-XSRF-TOKEN", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
source.registerCorsConfiguration("/secure/classifieds", configuration);
source.registerCorsConfiguration("/login", configuration);
source.registerCorsConfiguration("/logout", configuration);
return source;
}
}
RESTAuthenticationEntryPoint:
package org.adventure.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class RESTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
RESTAuthenticationFailureHandler
package org.adventure.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class RESTAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
super.onAuthenticationFailure(request, response, exception);
}
}
RESTAuthenticationSuccessHandler
package org.adventure.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class RESTAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
clearAuthenticationAttributes(request);
}
}
RESTLogoutSuccessHandler
package org.adventure.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class RESTLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
response.setStatus(HttpServletResponse.SC_OK);
}
}
Frontend:
To /login and /logout I make POST requests with withCredentials: true, and have a HttpInterceptor configured:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpXsrfTokenExtractor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class XsrfInterceptor implements HttpInterceptor {
constructor(private tokenExtractor: HttpXsrfTokenExtractor) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let requestToForward = req;
let token = this.tokenExtractor.getToken() as string;
if (token !== null) {
requestToForward = req.clone({ setHeaders: { "X-XSRF-TOKEN": token } });
}
return next.handle(requestToForward);
}
}
LogoutService:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class LogoutService {
private url = 'http://localhost:8080/logout';
constructor(private http: HttpClient) { }
public logout(): Observable<any> {
return this.http.post(
this.url, { withCredentials: true }).pipe(
map(response => {
console.log(response)
})
);
}
}
Upvotes: 1
Views: 2396
Reputation: 435
In HttpClient, the POST method has a little bit different signature than a GET: https://github.com/angular/angular/blob/master/packages/http/src/http.ts
The second parameter is any request body we want to send, not the options, which are the third parameter. So withCredentials: true was simply never set correctly on the actual request.
Changing the call to:
this.http.post(
this.url, null, { withCredentials: true })
fixed the problem.
Upvotes: 3