From c30bacac10b475f62891f234b3279148f5778281 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Wed, 14 Jun 2023 15:50:11 -0300 Subject: [PATCH] Improve Security Filters Documentation Closes gh-8167 --- .../ROOT/pages/servlet/architecture.adoc | 299 +++++++++++++++--- docs/spring-security-docs.gradle | 6 +- gradle.properties | 2 +- 3 files changed, 268 insertions(+), 39 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/architecture.adoc b/docs/modules/ROOT/pages/servlet/architecture.adoc index 962193212a..0ed5a73402 100644 --- a/docs/modules/ROOT/pages/servlet/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/architecture.adoc @@ -164,46 +164,224 @@ In fact, a `SecurityFilterChain` might have zero security ``Filter``s if the app [[servlet-security-filters]] == Security Filters +Spring Security uses a number of Servlet Filters (https://jakarta.ee/specifications/servlet/5.0/jakarta-servlet-spec-5.0.pdf[Jakarta Servlet Spec, Chapter 6]) to provide security to your application. The Security Filters are inserted into the <> with the <> API. -The <>s matters. +Those filters can be used for a number of different purposes, like xref:servlet/authentication/index.adoc[authentication], xref:servlet/authorization/index.adoc[authorization], xref:servlet/exploits/index.adoc[exploit protection], and more. +The filters are executed in a specific order to guarantee that they are invoked at the right time, for example, the `Filter` that performs authentication should be invoked before the `Filter` that performs authorization. It is typically not necessary to know the ordering of Spring Security's ``Filter``s. -However, there are times that it is beneficial to know the ordering +However, there are times that it is beneficial to know the ordering, if you want to know them, you can check the {gh-url}/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java[`FilterOrderRegistration` code]. -Below is a comprehensive list of Spring Security Filter ordering: +To exemplify the above paragraph, let's consider the following security configuration: + +==== +.Java +[source,java,role="primary"] +---- +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(Customizer.withDefaults()) + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .httpBasic(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()); + return http.build(); + } + +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +import org.springframework.security.config.web.servlet.invoke + +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + http { + csrf { } + authorizeHttpRequests { + authorize(anyRequest, authenticated) + } + httpBasic { } + formLogin { } + } + return http.build() + } + +} +---- +==== + +The above configuration will result in the following `Filter` ordering: + +[cols="1,1", options="header"] +|==== +| Filter | Added by +| xref:servlet/exploits/csrf.adoc[CsrfFilter] | `HttpSecurity#csrf` +| xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[UsernamePasswordAuthenticationFilter] | `HttpSecurity#formLogin` +| xref:servlet/authentication/passwords/basic.adoc[BasicAuthenticationFilter] | `HttpSecurity#httpBasic` +| xref:servlet/authorization/authorize-http-requests.adoc[AuthorizationFilter] | `HttpSecurity#authorizeHttpRequests` +|==== + +1. First, the `CsrfFilter` is invoked to protect against xref:servlet/exploits/csrf.adoc[CSRF attacks]. +2. Second, the authentication filters are invoked to authenticate the request. +3. Third, the `AuthorizationFilter` is invoked to authorize the request. + +[NOTE] +==== +There might be other `Filter` instances that are not listed above. +If you want to see the list of filters invoked for a particular request, you can <>. +==== + +[[servlet-print-filters]] +=== Printing the Security Filters + +Often times, it is useful to see the list of security ``Filter``s that are invoked for a particular request. +For example, you want to make sure that the <> is in the list of the security filters. + +The list of filters is printed at INFO level on the application startup, so you can see something like the following on the console output for example: + +[source,text,role="terminal"] +---- +2023-06-14T08:55:22.321-03:00 INFO 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ +org.springframework.security.web.session.DisableEncodeUrlFilter@404db674, +org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5, +org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7, +org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc, +org.springframework.security.web.csrf.CsrfFilter@c29fe36, +org.springframework.security.web.authentication.logout.LogoutFilter@ef60710, +org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2, +org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639, +org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c, +org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0, +org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774, +org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c, +org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff, +org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4, +org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7] +---- + +And that will give a pretty good idea of the security filters that are configured for <>. + +But that is not all, you can also configure your application to print the invocation of each individual filter for each request. +That is helpful to see if the filter you have added is invoked for a particular request or to check where an exception is coming from. +To do that, you can configure your application to <>. + +[[adding-custom-filter]] +=== Adding a Custom Filter to the Filter Chain + +Mostly of the times, the default security filters are enough to provide security to your application. +However, there might be times that you want to add a custom `Filter` to the security filter chain. + +For example, let's say that you want to add a `Filter` that gets a tenant id header and check if the current user has access to that tenant. +The previous description already gives us a clue on where to add the filter, since we need to know the current user, we need to add it after the authentication filters. + +First, let's create the `Filter`: + +[source,java] +---- +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.security.access.AccessDeniedException; + +public class TenantFilter implements Filter { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String tenantId = request.getHeader("X-Tenant-Id"); <1> + boolean hasAccess = isUserAllowed(tenantId); <2> + if (hasAccess) { + filterChain.doFilter(request, response); <3> + return; + } + throw new AccessDeniedException("Access denied"); <4> + } + +} + +---- + +The sample code above does the following: + +<1> Get the tenant id from the request header. +<2> Check if the current user has access to the tenant id. +<3> If the user has access, then invoke the rest of the filters in the chain. +<4> If the user does not have access, then throw an `AccessDeniedException`. + +[TIP] +==== +Instead of implementing `Filter`, you can extend from {spring-framework-api-url}org/springframework/web/filter/OncePerRequestFilter.html[OncePerRequestFilter] which is a base class for filters that are only invoked once per request and provides a `doFilterInternal` method with the `HttpServletRequest` and `HttpServletResponse` parameters. +==== + +Now, we need to add the filter to the security filter chain. + +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // ... + .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); <1> + return http.build(); +} +---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun filterChain(http: HttpSecurity): SecurityFilterChain { + http + // ... + .addFilterBefore(TenantFilter(), AuthorizationFilter::class.java) <1> + return http.build() +} +---- +==== + +<1> Use `HttpSecurity#addFilterBefore` to add the `TenantFilter` before the `AuthorizationFilter`. + +By adding the filter before the `AuthorizationFilter` we are making sure that the `TenantFilter` is invoked after the authentication filters. +You can also use `HttpSecurity#addFilterAfter` to add the filter after a particular filter or `HttpSecurity#addFilterAt` to add the filter at a particular filter position in the filter chain. + +And that's it, now the `TenantFilter` will be invoked in the filter chain and will check if the current user has access to the tenant id. + +Be careful when you declare your filter as a Spring bean, either by annotating it with `@Component` or by declaring it as a bean in your configuration, because Spring Boot will automatically {spring-boot-reference-url}web.html#web.servlet.embedded-container.servlets-filters-listeners.beans[register it with the embedded container]. +That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order. + +If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a `FilterRegistrationBean` bean and setting its `enabled` property to `false`: + +[source,java] +---- +@Bean +public FilterRegistrationBean tenantFilterRegistration(TenantFilter filter) { + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setEnabled(false); + return registration; +} +---- -* xref:servlet/authentication/session-management.adoc#session-mgmt-force-session-creation[`ForceEagerSessionCreationFilter`] -* ChannelProcessingFilter -* WebAsyncManagerIntegrationFilter -* SecurityContextPersistenceFilter -* HeaderWriterFilter -* CorsFilter -* CsrfFilter -* LogoutFilter -* OAuth2AuthorizationRequestRedirectFilter -* Saml2WebSsoAuthenticationRequestFilter -* X509AuthenticationFilter -* AbstractPreAuthenticatedProcessingFilter -* CasAuthenticationFilter -* OAuth2LoginAuthenticationFilter -* Saml2WebSsoAuthenticationFilter -* xref:servlet/authentication/passwords/form.adoc#servlet-authentication-usernamepasswordauthenticationfilter[`UsernamePasswordAuthenticationFilter`] -* OpenIDAuthenticationFilter -* DefaultLoginPageGeneratingFilter -* DefaultLogoutPageGeneratingFilter -* ConcurrentSessionFilter -* xref:servlet/authentication/passwords/digest.adoc#servlet-authentication-digest[`DigestAuthenticationFilter`] -* BearerTokenAuthenticationFilter -* xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[`BasicAuthenticationFilter`] -* <> -* SecurityContextHolderAwareRequestFilter -* JaasApiIntegrationFilter -* RememberMeAuthenticationFilter -* AnonymousAuthenticationFilter -* OAuth2AuthorizationCodeGrantFilter -* SessionManagementFilter -* <> -* xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] -* SwitchUserFilter [[servlet-exceptiontranslationfilter]] == Handling Security Exceptions @@ -333,3 +511,52 @@ XML:: === RequestCacheAwareFilter The {security-api-url}org/springframework/security/web/savedrequest/RequestCacheAwareFilter.html[`RequestCacheAwareFilter`] uses the <> to save the `HttpServletRequest`. + +[[servlet-logging]] +== Logging + +Spring Security provides comprehensive logging of all security related events at the DEBUG and TRACE level. +This can be very useful when debugging your application because for security measures Spring Security does not add any detail of why a request has been rejected to the response body. +If you come across a 401 or 403 error, it is very likely that you will find a log message that will help you understand what is going on. + +Let's consider an example where a user tries to make a `POST` request to a resource that has xref:servlet/exploits/csrf.adoc[CSRF protection] enabled without the CSRF token. +With no logs, the user will see a 403 error with no explanation of why the request was rejected. +However, if you enable logging for Spring Security, you will see a log message like this: + +[source,text] +---- +2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello +2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15) +2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15) +2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15) +2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15) +2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15) +2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://localhost:8080/hello +2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code +2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure] +---- + +It becomes clear that the CSRF token is missing and that is why the request is being denied. + +To configure your application to log all the security events, you can add the following to your application: + +==== +.application.properties in Spring Boot +[source,properties,role="primary"] +---- +logging.level.org.springframework.security=TRACE +---- +.logback.xml +[source,xml,role="secondary"] +---- + + + + + + + + + +---- +==== diff --git a/docs/spring-security-docs.gradle b/docs/spring-security-docs.gradle index 792fd817e9..4497c2e2b2 100644 --- a/docs/spring-security-docs.gradle +++ b/docs/spring-security-docs.gradle @@ -40,7 +40,8 @@ def generateAttributes() { def securityReferenceUrl = "$securityDocsUrl/reference/html5/" def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/" def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/" - + def springBootReferenceUrl = "https://docs.spring.io/spring-boot/docs/$springBootVersion/reference/html/" + return ['gh-old-samples-url': ghOldSamplesUrl.toString(), 'gh-samples-url': ghSamplesUrl.toString(), 'gh-url': ghUrl.toString(), @@ -48,7 +49,8 @@ def generateAttributes() { 'security-reference-url': securityReferenceUrl.toString(), 'spring-framework-api-url': springFrameworkApiUrl.toString(), 'spring-framework-reference-url': springFrameworkReferenceUrl.toString(), - 'spring-security-version': project.version] + 'spring-boot-reference-url': springBootReferenceUrl.toString(), + 'spring-security-version': project.version] + resolvedVersions(project.configurations.testRuntimeClasspath) } diff --git a/gradle.properties b/gradle.properties index 6d3fb9547e..31f3a26d4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ aspectjVersion=1.9.19 springJavaformatVersion=0.0.39 -springBootVersion=2.4.2 +springBootVersion=2.7.12 springFrameworkVersion=5.3.28 openSamlVersion=3.4.6 version=5.8.5-SNAPSHOT