Introduction

Java is the first language I learned and have used for a long time. Every time when I want to study a new technology or concept. I always look at Java because it often has the most implemented Libraries with well-structured readable codebase.

This time, I want to learn all about the web securities, JWT, OAuth2 and SAML2. To study web security technologies. I will use SpringBoot’s web security package. I will go through the package source code and alongside with the sample projects from Spring-security‘s github repositary. I am writing this post step by step while I learn, so maybe the overall structure may not be very readable. But I will try.

Step 1: Look at starter package

To use Spring-security, we have to load in the spring-boot-starter-security packge using maven, this can be done automatically if you setup the project using spring.io

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

starter autoconfiguration

Nagivating into and see what the starter package contains using Intellij. The package contains spring-boot-starter, spring-aop, spring-security-config, spring-security-web. Among this, we are interested in

  • spring-boot-autoconfigure which is in spring-boot-starter,
  • spring-security-core and spring-web which is in both spring-security-config and spring-security-web,

I have learned that to study a Spring Boot technology, the first step is always to look at its corresponding autoconfiguration. For spring-security, autoconfiguration is under org.springframework.boot.autoconfigure.security. The folder contains:

1
2
3
4
5
6
7
8
9
10
11
org.springframework.boot.autoconfigure.security
├── oauth2/ # oauth2 config
├── reactive/ # I guess for webflux security config?
├── rsocket/ # I guess for rsocket.io config, never heard of this
├── saml2/ # saml2 config
├── servlet/ # web controller config.
├── ConditionalOnDefaultWebSecurity.java
├── DefaultWebSecurityCondition.java
├── SecurityDataConfiguration.java
├── SecurityProperties.java
├── StaticResourceLocation.java

DefaultWebSecurityCondition and ConditionalOnDefaultWebSecurity

At first, the class DefaultWebSecurityCondition catches my eyes because of the “Default” keyword. Looking at the source

DefaultWebSecurityCondition.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* {@link Condition} for
* {@link ConditionalOnDefaultWebSecurity @ConditionalOnDefaultWebSecurity}.
*
* @author Phillip Webb
*/
class DefaultWebSecurityCondition extends AllNestedConditions {

DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {

}

@ConditionalOnMissingBean({ SecurityFilterChain.class })
static class Beans {

}
}

The class is a AllNestedConditions which means that this is a composite condition where:

  • @ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class }):
    Both SecurityFilterChain.class and HttpSecurity.class must exist in classpath. They are both in the spring-web package which is included earlier.
  • @ConditionalOnMissingBean({ SecurityFilterChain.class })
    This condition is also because, A little spoiler. SecurityFilterChain is a bean that should be configured by developer. Since we have not do anything yet, the bean is missing in the Spring container.

And based on the class comment, this condition is used for ConditionalOnDefaultWebSecurity, which is used as a @Document annotation.

Other files

Next, lets look at other files:

  • SecurityDataConfiguration:
    The class comment says

    Automatically adds Spring Security’s integration with Spring Data.

    which is irrelavent to our objective, skipped.
  • StaticResourceLocation:
    Defines some constant enums for file pattern paths. For example, “CSS(“/css/**“)", “JAVA_SCRIPT(“/js/**“)”, IMAGES(“/images/**“) etc.
  • SecurityProperties:
    This one is interesting, the class contains two inner class Filter and User and some default values
    SecurityProperties.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /**
    * Default order of Spring Security's Filter in the servlet container (i.e. amongst
    * other filters registered with the container). There is no connection between this
    * and the {@code @Order} on a {@code SecurityFilterChain}.
    */
    public static final int DEFAULT_FILTER_ORDER = OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER - 100;

    private final Filter filter = new Filter();

    private final User user = new User();

    public User getUser() {
    return this.user;
    }

    public Filter getFilter() {
    return this.filter;
    }

    public static class Filter {

    /**
    * Security filter chain order for Servlet-based web applications.
    */
    private int order = DEFAULT_FILTER_ORDER;

    /**
    * Security filter chain dispatcher types for Servlet-based web applications.
    */
    private Set<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);

    // ... getters and setters
    }

    public static class User {

    /**
    * Default user name.
    */
    private String name = "user";

    /**
    * Password for the default user name.
    */
    private String password = UUID.randomUUID().toString();

    /**
    * Granted roles for the default user name.
    */
    private List<String> roles = new ArrayList<>();

    private boolean passwordGenerated = true;

    // ... getters and setters
    }
    The user class contains some default values and the newed user is not a bean. So I guess it is save to ignore at the moment? And also, I guess that is why you cannot define your own User class because it will causes conflict with the class here.

servlet/

servlet/ folder contains the autoconfiguration for controllers. It contains

1
2
3
4
5
6
7
8
9
servlet/
├── AntPathRequestMatcherProvider.java
├── PathRequest.java
├── RequestMatcherProvider.java
├── SecurityAutoConfiguration.java
├── SecurityFilterAutoConfiguration.java
├── SpringBootWebSecurityConfiguration.java
├── StaticResourceRequest.java
├── UserDetailsServiceAutoConfiguration.java
  • AntPathRequestMatcherProvider implements RequestMatcherProvider interface and provide an instance of AntPathRequestMatcher from the spring-web package
  • PathRequest delegates some commonly used paths for web resources, like StaticResourceRequest.
  • SecurityAutoConfiguration:
    The class only contains @ConditionalOnMissingBean AuthenticationEventPublisher. The class must be configured before @AutoConfiguration(before = UserDetailsServiceAutoConfiguration.class). It seems like this is related to the implementation of Authentication. Put this class on hold for now.
  • SecurityFilterAutoConfiguration:
    • Based on the class comment, the class

      ensure that the filter’s order is still configured when a user-provided WebSecurityConfiguration exists.

    • The class is configured after @AutoConfiguration(after = SecurityAutoConfiguration.class). So this configuration happens last?
    • It imports SecurityProperties using @EnableConfigurationProperties(SecurityProperties.class).
    • Looking at the source code for this:
      SecurityFilterAutoConfiguration.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      @Bean
      @ConditionalOnBean(name = DEFAULT_FILTER_NAME)
      public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
      SecurityProperties securityProperties) {
      DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
      DEFAULT_FILTER_NAME);
      registration.setOrder(securityProperties.getFilter().getOrder());
      registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
      return registration;
      }
      The configuration creates a new DelegatingFilterProxyRegistrationBean object using the default Filter object in SecurityProperties. The comment in DelegatingFilterProxyRegistrationBean suggests

      When no URL pattern or servlets are specified the filter will be associated to ‘/*’.”. So I can guess that the this filter is responsible for all remaining unmatched URLs.

  • SpringBootWebSecurityConfiguration:
    Title says it all
    SpringBootWebSecurityConfiguration.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class SpringBootWebSecurityConfiguration {

    /**
    * The default configuration for web security. It relies on Spring Security's
    * content-negotiation strategy to determine what sort of authentication to use. If
    * the user specifies their own {@link SecurityFilterChain} bean, this will back-off
    * completely and the users should specify all the bits that they want to configure as
    * part of the custom security configuration.
    */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {

    @Bean
    @Order(SecurityProperties.BASIC_AUTH_ORDER)
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
    http.formLogin(withDefaults());
    http.httpBasic(withDefaults());
    return http.build();
    }

    }

    /**
    * Adds the {@link EnableWebSecurity @EnableWebSecurity} annotation if Spring Security
    * is on the classpath. This will make sure that the annotation is present with
    * default security auto-configuration and also if the user adds custom security and
    * forgets to add the annotation. If {@link EnableWebSecurity @EnableWebSecurity} has
    * already been added or if a bean with name
    * {@value BeanIds#SPRING_SECURITY_FILTER_CHAIN} has been configured by the user, this
    * will back-off.
    */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
    @ConditionalOnClass(EnableWebSecurity.class)
    @EnableWebSecurity
    static class WebSecurityEnablerConfiguration {

    }
    }
    • WebSecurityEnablerConfiguration has a @EnableWebSecurity annotation that introduction more specific security configuration for spring-web. More details on next section.
    • SecurityFilterChainConfiguration is configured when condition @ConditionalOnDefaultWebSecurity is true, in which is described above when there is no SecurityFilterChain in the bean container. Just by looking
      • http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        can be intepreted as all URL resources must be authenticated before access.
      • http.formLogin(withDefaults()); http.httpBasic(withDefaults());
        can be intepreted that this filter will implement a default form authentication and http authentication.
      • The interesting thing here is the withDefaults() method. The method returns a default argument required to supply the method.
        Customizer
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        @FunctionalInterface
        public interface Customizer<T> {

        /**
        * Performs the customizations on the input argument.
        * @param t the input argument
        */
        void customize(T t);

        /**
        * Returns a {@link Customizer} that does not alter the input argument.
        * @return a {@link Customizer} that does not alter the input argument.
        */
        static <T> Customizer<T> withDefaults() {
        return (t) -> {
        };
        }

        }

        The generic type T allows withDefaults() to be used for different methods.
    • UserDetailsServiceAutoConfiguration
      • The configuration provides an implementation of AuthenticationManager (acting as a Service bean) that is in-memory using HashMap.
        org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        @Bean
        public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
        ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(User.withUsername(user.getName())
        .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
        .roles(StringUtils.toStringArray(roles))
        .build());
        }

        private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) { ... }

        static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition {

        MissingAlternativeOrUserPropertiesConfigured() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @ConditionalOnMissingClass({
        "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
        "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
        "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
        static final class MissingAlternative {

        }

        @ConditionalOnProperty(prefix = "spring.security.user", name = "name")
        static final class NameConfigured {

        }

        @ConditionalOnProperty(prefix = "spring.security.user", name = "password")
        static final class PasswordConfigured {

        }
        }
      • It uses the User object created in SecurityProperties and puts it into InMemoryUserDetailsManager. Lets work on details of InMemoryUserDetailsManager later.
      • It allows to set custom values for spring.security.user.name and spring.security.user.password in properties.yaml.

@EnableWebSecurity

Previous section describe the autoconfiguration for the starter package, which is the foundation block of spring security. @EnableWebSecurity describes how to configure web related seucirty. This is like middleware. You know, in older time, we have AOP (Aspect oriented programming) where we can define filters for Http requests that choose to allow/deny a particular request. At least, this is how I understand this at first place.

@EnableWebSecurity
1
2
3
4
5
6
7
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {
...
}

The annotation import WebSecurityConfiguration, SpringWebMvcImportSelector, OAuth2ImportSelector and EnableGlobalAuthentication which imports AuthenticationConfiguration.

SpringWebMvcImportSelector and OAuth2ImportSelector seems to import other packages when spring-web-mvc or spring-oauth2-client is included in maven.

WebSecurityConfiguration

Based on the comment, the class

uses WebSecurity to reate the FilterChainProxy that performs the web based security for Spring Security. It then exports the necessary beans.

WebSecurityConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;

private Boolean debugEnabled;

private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();

private List<WebSecurityCustomizer> webSecurityCustomizers = Collections.emptyList();

private ClassLoader beanClassLoader;

@Bean
public static DelegatingApplicationListener delegatingApplicationListener() {
return new DelegatingApplicationListener();
}

@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public SecurityExpressionHandler<FilterInvocation> webSecurityExpressionHandler() {
return this.webSecurity.getExpressionHandler();
}

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
// ...
}

@Bean
@DependsOn(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public WebInvocationPrivilegeEvaluator privilegeEvaluator() {
return this.webSecurity.getPrivilegeEvaluator();
}
// rest are Autowired setters for securityFilterChains, webSecurityCustomizers etc.
}

Lets look at each bean in detail:
DelegatingApplicationListener: based on the name, the bean is probably related to ApplicationListener in spring-core, which is probably irrelavent to spring-security.

WebInvocationPrivilegeEvaluator() and SecurityExpressionHandler() both depends on springSecurityFilterChain().

WebSecurityConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
if (!hasFilterChain) {
this.webSecurity.addSecurityFilterChainBuilder(() -> {
this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());
this.httpSecurity.formLogin(Customizer.withDefaults());
this.httpSecurity.httpBasic(Customizer.withDefaults());
return this.httpSecurity.build();
});
}
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}

In servlet/SpringBootWebSecurityConfiguration, the class created a default SecurityFilterChain object and put into spring container. So here, it guarantees that bool hasFilterChain is always true so the if statement is ignored. The rest of the code is just adding SecurityFilterChain and WebSecurityCustomizer to the webSecurity object.

Debugging using Intellij, we see that at startup, this.securityFilterChains is only size of 1.
debug_SpringBootWebSecurityConfiguration

HttpSecurityConfiguration

HttpSecurityConfiguration primarliy implements a HttpSecurity httpSecurity() bean.

httpSecurity()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
this.objectPostProcessor, passwordEncoder);
authenticationBuilder.parentAuthenticationManager(authenticationManager());
authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
// @formatter:off
http
.csrf(withDefaults())
.addFilter(webAsyncManagerIntegrationFilter)
.exceptionHandling(withDefaults())
.headers(withDefaults())
.sessionManagement(withDefaults())
.securityContext(withDefaults())
.requestCache(withDefaults())
.anonymous(withDefaults())
.servletApi(withDefaults())
.apply(new DefaultLoginPageConfigurer<>());
http.logout(withDefaults());
// @formatter:on
applyCorsIfAvailable(http);
applyDefaultConfigurers(http);
return http;
}

Code has

  • prototype scope annotation, meaning every http request will create a HttpSecurity object.
  • LazyPasswordEncoder: for encoding password. class LazyPasswordEncoder {} is alao in HttpSecurityConfiguration.java
  • AuthenticationManagerBuilder:
    Based on the comment

    Allows for easily building in memory authentication, LDAP authentication, JDBC based authentication, adding UserDetailsService, and adding AuthenticationProvider’s.
    This is related to authentication, which will go deeper later on.

  • WebAsyncManagerIntegrationFilter: Something related for spring-web’s asynchronous filter.
    • The class extends class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter. Skimming through, OncePerRequestFilter guarantees a single execution per request dispatch, on any servlet container. Interesting.

Rest of the code specifies defaults for the HttpSecurity object. Looking back, in SpringBootWebSecurityConfiguration, the class has a SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) that adds defaults for formLogin() and httpBasic(). So just guessing, I would say the last element of this.securityFilterChains in WebSecurityConfiguration will include a HttpSecurity will all defaults configured in HttpSecurityConfiguration plus default formLogin() and default httpBasic().

AuthenticationConfiguration

AuthenticationConfiguration provides implementation for DefaultPasswordEncoderAuthenticationManagerBuilder and LazyPasswordEncoder. Name tells it all and nothing special here.

Conclusion

Here, that concludes everything in the autoconfiguration side. Next, I want to dive into the code samples in Github and actually implement some security measures.