Spring (Boot) application and csrf - spring-security

I use this statements in the WebSecurityConfigurerAdapter#configure method to secure my application.
If csrf is disabled (http.csrf().disable();) than everything works fine, if I remove this line than I get an exception (Forbidden 403).
It is not clear for me why - I want to be safe against csrf- attack.
Does anyone know what I am doing wrong?
#Override
protected void configure(final HttpSecurity http) throws Exception {
final List<String> activeProfiles = Arrays.asList(environment.getActiveProfiles());
final CsrfTokenResponseHeaderBindingFilter csrfFilter = csrfTokenResponseHeaderBindingFilter();
http.addFilterAfter(csrfFilter, CsrfFilter.class).headers().cacheControl().xssProtection();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
final String[] restEndpointsToSecure = WebSecurityConfig.restEndpointsToSecure;
for (final String endpoint : restEndpointsToSecure) {
http.authorizeRequests().antMatchers("/" + endpoint + "/**").hasRole(UserRoleEnum.USER.toString());
}
xAuthTokenConfigurer.setDetailsService(userDetailsServiceBean());
final SecurityConfigurer<DefaultSecurityFilterChain, HttpSecurity> securityConfigurerAdapter = xAuthTokenConfigurer;
http.apply(securityConfigurerAdapter);
}

Spring Security CSRF Documentation
You need to include the token in your requests. If you use Thymeleaf as your templating engine, this is handled automatically. The documentation also describes how to handle Ajax as well.

Related

User Impersonation with Spring Security

I am trying to implement a user impersonation using Spring Security and its SwitchUserFilter.
Currently the Configuration looks as follows:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/impersonate*").hasRole("Administrator");
http.addFilter(switchUserFilter());
super.configure(http);
setLoginView(http, ViewLogin.class);
}
...
#Bean
public SwitchUserFilter switchUserFilter(){
SwitchUserFilter filter = new SwitchUserFilter();
filter.setUserDetailsService(userDetailsService);
filter.setSwitchUserUrl("/impersonate");
filter.setSwitchFailureUrl("/switchUser");
filter.setTargetUrl("/");
return filter;
}
And I was trying to navigate to the impersonation using:
UI.getCurrent().getPage().setLocation("/impersonate?username="+username);
Unfortunately Vaadin is trying to navigate to the page "/impersonate..." but doesn't find a corresponding Page and skips the SwitchUserFilter.
How would I force the switch?
If you use setSwitchUserUrl it will match only POST requests [1].
But you want to use a GET request. So you have to use a matcher like
this:
filter.setSwitchUserMatcher(new AntPathRequestMatcher("/impersonate", "GET"));
BTW: you don't have to addFilter the filter, if you define it as
a #Bean.
[1]
https://github.com/spring-projects/spring-security/blob/e125a76687d4ca9739cd663eedc107c7ff55e8cf/web/src/main/java/org/springframework/security/web/authentication/switchuser/SwitchUserFilter.java#L513-L515

How to properly configure spring-security with vaadin14 to handle 2 entry points - keyclaok and DB

I have a vaadin14 application that I want to enable different types of authentication mechanisms on different url paths. One is a test url, where authentication should use DB, and the other is the production url that uses keycloak.
I was able to get each authentication mechanism to work separately, but once I try to put both, I get unexpected results.
In both cases, I get login page, but the authentication doesn't work correctly. Here's my security configuration, what am I doing wrong?
#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfiguration {
#Configuration
#Order(2)
public static class DBAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String LOGIN_PROCESSING_URL = "/login";
private static final String LOGIN_FAILURE_URL = "/login?error";
private static final String LOGIN_URL = "/login";
private static final String LOGOUT_SUCCESS_URL = "/login";
/**
* Require login to access internal pages and configure login form.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
// Not using Spring CSRF here to be able to use plain HTML for the login page
http.csrf().disable()
// Register our CustomRequestCache, that saves unauthorized access attempts, so
// the user is redirected after login.
.requestCache().requestCache(new CustomRequestCache())
// Restrict access to our application.
.and().antMatcher("/test**").authorizeRequests()
// Allow all flow internal requests.
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
// Allow all requests by logged in users.
.anyRequest().hasRole("USER")
// Configure the login page.
.and().formLogin().loginPage(LOGIN_URL).permitAll().loginProcessingUrl(LOGIN_PROCESSING_URL)
.failureUrl(LOGIN_FAILURE_URL)
// Configure logout
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
}
#Bean
#Override
public UserDetailsService userDetailsService() {
Properties users = null;
try {
users = PropertiesLoaderUtils.loadAllProperties("users.properties");
return new InMemoryUserDetailsManager(users);
} catch (IOException e) {
e.printStackTrace();
}
UserDetails user =
User.withUsername("user")
.password("{noop}password")
.roles("ACTOR")
.build();
return new InMemoryUserDetailsManager(user);
}
/**
* Allows access to static resources, bypassing Spring security.
*/
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
// Vaadin Flow static resources
"/VAADIN/**",
// the standard favicon URI
"/favicon.ico",
// the robots exclusion standard
"/robots.txt",
// web application manifest
"/manifest.webmanifest",
"/sw.js",
"/offline-page.html",
// icons and images
"/icons/**",
"/images/**",
// (development mode) static resources
"/frontend/**",
// (development mode) webjars
"/webjars/**",
// (development mode) H2 debugging console
"/h2-console/**",
// (production mode) static resources
"/frontend-es5/**", "/frontend-es6/**",
"/resources/**");
}
}
#Order(1)
#Configuration
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public static class AppKeycloakSecurity extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(
AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider
= keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(
new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.httpBasic().disable();
http.formLogin().disable();
http.anonymous().disable();
http.csrf().disable();
http.headers().frameOptions().disable();
http
.antMatcher("/prod**")
.authorizeRequests()
.antMatchers("/vaadinServlet/UIDL/**").permitAll()
.antMatchers("/vaadinServlet/HEARTBEAT/**").permitAll()
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
.anyRequest().hasRole("actor");
http
.logout()
.addLogoutHandler(keycloakLogoutHandler())
.logoutUrl("/logout").permitAll()
.logoutSuccessUrl("/");
http
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class);
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy());
}
}
}
Navigating within a Vaadin UI will change the URL in your browser, but it will not necessarily create a browser request to that exact URL, effectively bypassing the access control defined by Spring security for that URL. As such, Vaadin is really not suited for the request URL-based security approach that Spring provides. For this issue alone you could take a look at my add-on Spring Boot Security for Vaadin which I specifically created to close the gap between Spring security and Vaadin.
But while creating two distinct Spring security contexts based on the URL is fairly easy, this - for the same reason - will not work well or at all with Vaadin. And that's something even my add-on couldn't help with.
Update: As combining both security contexts is an option for you, I can offer the following solution (using my add-on):
Starting from the Keycloak example, you would have to do the following:
Change WebSecurityConfig to also add your DB-based AuthenticationProvider. Adding your UserDetailsService should still be enough. Make sure to give every user a suitable role.
You have to remove this line from application.properties: codecamp.vaadin.security.standard-auth.enabled = false
This will re-enable the standard login without Keycloak via a Vaadin view.
Adapt the KeycloakRouteAccessDeniedHandler to ignore all test views that shouldn't be protected by Keycloak.
I already prepared all this in Gitlab repo and removed everything not important for the main point of this solution. See the individual commits and their diffs to also help focus in on the important bits.

How to make certain endpoint in spring security to be allowed without authentication?

I'm developing an application using Spring security.
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.csrf().disable().authorizeRequests().antMatchers("/api/client/findByVariable?variable=").permitAll();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().requestMatchers()
.antMatchers("/api/**").and().authorizeRequests().antMatchers("/api/**")
.access("#oauth2.hasScope('read') or (!#oauth2.isOAuth() and hasRole('USER'))");
// #formatter:on
}
How can I modify above code so that endpoint "/api/client/findByVariable?variable=" can be allowed to be accessed without requiring authentication just as if there was no Spring Security ?
I tried adding the line :
http.csrf().disable().authorizeRequests().antMatchers("/api/client/findByVariable?variable=").permitAll();
But it is not working
You can ignore endpoints with overriding the configure method that gives you a WebSecurity instance:
#Override
public void configure(WebSecurity web)
{
web.ignoring().antMatchers("/api/client/findByVariable");
}
I'm not sure if you can also match by query params like /api/client/findByVariable?variable=*

Spring Boot securing same endpoint with CSRF token and BasicAuth

I have a Spring Boot REST application which has two main parts:
UI where I want to protect the ajax calls with a token
public endpoints where I want to have Basic Auth
As far as I understand I can't protect the public endpoints with CSRF tokens, as these need a session. The problem is, some endpoints need to be reachable by both, so how can I protect them with CSRF when it is used by the UI and disable CSRF for Basic Auth?
Here is what I currently have, where I disable csrf completely so basic works...
http.requestMatchers().antMatchers("/form/fill", "/form/fill/*", "/form/fillParams", "/form/fillParams/*").and()
.csrf().disable().authorizeRequests().anyRequest().hasAnyRole(SecurityConfiguration.ROLE_FORMS_AUTHOR,
SecurityConfiguration.ROLE_FORM_FILLER, SecurityConfiguration.ROLE_ADMIN)
.and().httpBasic();
EDIT: I found this old answer and I wonder if there is a way I can leverage this for my case, but I'm still not sure how to distinguish between a "local" user and one that is authenticated with httpBasic()
In your Spring Security java configuration file you can configure the HttpSecurity object as follows in order to enable the CSRF check only on some requests (by default is enabled on all the incoming requests, and disable will disable for all incoming request so request Mather can help here for path you want to enable or disable csrf.).
Make sure to replace /urls-with-csrf-check/** with your paths by end point or multiple paths..
#Override
protected void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
private RegexRequestMatcher requestMatcher =
new RegexRequestMatcher("/urls-with-csrf-check/**", null);
public boolean matches(HttpServletRequest httpServletRequest) {
if (requestMatcher.matches(httpServletRequest)) {
return true;
}
return false;
}
};
http.requestMatchers().antMatchers("/form/fill", "/form/fill/*", "/form/fillParams", "/form/fillParams/*").and()
.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
.authorizeRequests().anyRequest().hasAnyRole(SecurityConfiguration.ROLE_FORMS_AUTHOR, SecurityConfiguration.ROLE_FORM_FILLER, SecurityConfiguration.ROLE_ADMIN)
.and().httpBasic();
}
With the input from #kj007, I was able to get this working.
I am using the requireCsrfProtectionMatcher and this is how my matcher looks like:
public class UIRequestMatcher implements RequestMatcher {
public static final List<GrantedAuthority> USER_ROLES = new ArrayList<>();
static {
USER_ROLES.add(new SimpleGrantedAuthority(SecurityConfiguration.ROLE_ADMIN));
USER_ROLES.add(new SimpleGrantedAuthority(SecurityConfiguration.ROLE_FILES_AUTHOR));
USER_ROLES.add(new SimpleGrantedAuthority(SecurityConfiguration.ROLE_FORMS_AUTHOR));
USER_ROLES.add(new SimpleGrantedAuthority(SecurityConfiguration.ROLE_TEMPLATES_AUTHOR));
}
#Override
public boolean matches(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return "POST".equals(request.getMethod()) && auth.getAuthorities().stream().anyMatch(USER_ROLES::contains);
}
}
So I am checking if the Authentication has any of my user roles, as my basic auth should only be used for my technical users.

Enabling Spring Security makes Swagger output text/plain instead of HTML

Swagger works! I can interact with http://localhost:8090/sdoc.jsp and everything is fine.
I add the following to pom.xml...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
I also add the following two files:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
if( !Authenticate.authenticate(name, password) )
return null;
List<GrantedAuthority> grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
Authentication auth = new UsernamePasswordAuthenticationToken(name, password, grantedAuths);
return auth;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
and
#Configuration
#EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.anyRequest().permitAll()
.antMatchers("/**").authenticated().and()
.formLogin().loginPage("/login").permitAll().and()
.httpBasic()
;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(new CustomAuthenticationProvider());
}
}
At this point if I visit the same URL that was previously working I now instead get a response type of "text/plain" and instead of a pretty HTML looking browser I see source code.
If I revert the change and remove the two files from project and remove JAR file it works again.
How do I get Spring Security and Swagger to play nice? What am I doing wrong.
I suspect this is due to Spring-Security's effect on the content-type headers (http://docs.spring.io/spring-security/site/docs/3.2.0.CI-SNAPSHOT/reference/html/headers.html#headers-content-type-options).
From the docs -
Historically browsers, including Internet Explorer, would try to guess the content type of a request using content sniffing. This allowed browsers to improve the user experience by guessing the content type on resources that had not specified the content type. For example, if a browser encountered a JavaScript file that did not have the content type specified, it would be able to guess the content type and then execute it.
The problem with content sniffing is that this allowed malicious users to use polyglots (i.e. a file that is valid as multiple content types) to execute XSS attacks. For example, some sites may allow users to submit a valid postscript document to a website and view it. A malicious user might create a postscript document that is also a valid JavaScript file and execute a XSS attack with it.
Again, from the docs, in order to override the default -
#EnableWebSecurity
#Configuration
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers()
.contentTypeOptions();
}
}
Wow, I figured it was something along these lines. Thanks so much
When I tried this and it started working
.headers()
.disable()
I narrowed the default contentTypeOptions down to..
.headers()
//.contentTypeOptions() // If this is uncommented it fails.
.xssProtection()
.cacheControl()
.httpStrictTransportSecurity()
.frameOptions()
.and()

Resources