Introduction

This time lets get hands-on spring security. Based on the previous post, there are two parts needed to setup a proper security measure for a website, SecurityFilterChain and UserDetailsManager.

SecurityFilterChain is a set of filters that determines if accessing a particular URL is allowed or not. UserDetailsManager, as the name suggests, performs CRUD actions regarding users.

hello-security

Checking out the example from hello-security-explicit

SecurityConfiguration.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
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
// @formatter:on
return http.build();
}

// @formatter:off
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// @formatter:on
}

The code abocve shows:

  1. To customize spring security configuration, add @EnableWebSecurity to inherit default spring-security configurations. Because we define our own SecurityFilterChain bean, class DefaultWebSecurityCondition extends AllNestedConditions {} from previous post will become false and hence defaultSecurityFilterChain in SpringBootWebSecurityConfiguration.java will not get created. However, here, if you look at the implementation of defaultSecurityFilterChain, it is the same thing from example code above.

  2. Because we define our own InMemoryUserDetailsManager, UserDetailsServiceAutoConfiguration will not be created because of @ConditionalOnMissingBean(value = {…, UserDetailsService.class})

  3. The InMemoryUserDetailsManager is very similar to the default one in UserDetailsServiceAutoConfiguration.java. Here, I want to dive deeper in the password encoder. Based on the official docs, spring security requires each password to be prefixed using “{encoder_format}”. For example, a proper way to store a password should be “{noop}password” (no encoding) or “{bcrypt}password” {using bcrypt algorithm encoding}. The createDelegatingPasswordEncoder() method in org.springframework.security.crypto.factory.PasswordEncoderFactories returns a list of default password encoders used by spring security.

    PasswordEncoderFactories.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public static PasswordEncoder createDelegatingPasswordEncoder() {
    String encodingId = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(encodingId, new BCryptPasswordEncoder());
    encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
    encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
    encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
    encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
    encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
    encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
    encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
    encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
    encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
    encoders.put("SHA-256",
    new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
    encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
    encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
    encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
    return new DelegatingPasswordEncoder(encodingId, encoders);
    }

    If you create a UserDetails without a password encoder, for example UserDetails user = User.withUsername("user").password("password").build();, Java will throw

    java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”.

Custom defined User

In the real world application, it is often that our User will contain more domain knowledge, such as email, address etc. How to integrate this information is really important. Let’s take a look at the InMemoryUserDetailsManager class. The class implements two interfaces UserDetailsManager which in turn also implements UserDetailsService and UserDetailsPasswordService.

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface UserDetailsPasswordService {
UserDetails updatePassword(UserDetails user, String newPassword);
}

public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);

void updateUser(UserDetails user);

void deleteUser(String username);

void changePassword(String oldPassword, String newPassword);

boolean userExists(String username);
}

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

To replicate what InMemoryUserDetailsManager has done (we don’t care about changing the password, so UserDetailsPasswordService is ignored).As an example, lets define a custom User class that uses Email as username for login. Lombok is used here. The code is similar to the User.class in org.springframework.security.core.userdetails.User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
@AllArgsConstructor
public class CustomUser {
private String email;
private String password;
}

public interface CustomUserServices {
CustomUser findCustomUserByEmail(String email);
// additional CRUD ignored.
}

@AllArgsConstructor
public class CustomUserRepository implements CustomUserServices {
private final Map<String, CustomUser> users = new HashMap<>();

@Override
public CustomUser findCustomUserByEmail(String email) {
return this.users.get(email);
}
// additional CRUD implementation ignored.
}

Here, we have a CustomUser class holding email and password, and repository storing lists of CustomUser. Now, to comply with spring security, we need a UserDetail class to specify the role/authority of our CustomUser.

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
@Data
@AllArgsConstructor
public class CustomUserDetail implements UserDetails {
private CustomUser customUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AuthorityUtils.NO_AUTHORITIES;
}

@Override
public String getPassword() {
return customUser.getPassword();
}

@Override
public String getUsername() {
return customUser.getEmail();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

The CustomUserDetail has no authority/role and is always valid. Here, instead of inheritance, I use composition. I feel that composition is easier to understand because it separates the user domain and the authority domain, the single responsibility principle.

I guess another way to make it more appropriate in turns of database design, is to make User class and UserDetail class standalone by themselves and have a third class joins them. But I guess that needs more experiments to see what works the best.

Enough with the sidetrack thought. Now, we need a service class that implements UserDetailsService.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@AllArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private CustomUserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUser customUser = userRepository.findCustomUserByEmail(username);
return new CustomUserDetail(customUser);
}
}

@Bean
public CustomUserRepository userRepository() {
Map<String, CustomUser> users = new HashMap<>();
String encodedPassword = PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("password");
CustomUser customUser = new CustomUser("user@mail.com", encodedPassword);
users.put(customUser.getEmail(), customUser);
return new CustomUserRepository(users);
}

On top, we initialized a repository that hold CustomUser login credential, noted that the CustomUserRepository Bean must be put under a class with @Configuration/@Document. Now if we run the application, we can use “user@mail.com” and “password” for login.

Conclusion

To configure custom information to login, you need to

  1. Obviously, define the class that contains the custom information, the class must either implement UserDetails interface (direct inheritance) or have a different class that implements UserDetails to include the class (composition).
  2. Define a repository class to store you information
  3. Define a service/manager class that implements UserDetailsService interface or UserDetailsManager interface. The spring’s underlying code will call the loadUserByUsername() method to look if the credential exists or not.
    • The service/manager will use the repository to retrieve user by its key(username/email etc).

Until here, I have a question in my mind, where in code does spring security check the password?

Question: How is Authentication done in spring security?

This is the question I realize when I was writing the previous section. Because loadUserByUsername() method only returns a UserDetail object, where does the password comparison happened? To understand this problem, first we need to know how HTTP request is handled in Spring. So long story short, when the spring server receives an HTTP request, it will undergo a bunch of filters (called FilterChain), such as form character encoding filter, form value parsing filter etc., in ApplicationFilterChain.java in org.apache.catalina.core package.
ApplicationFilterChain

DelegatingFilterProxy

The only thing we are interested here in the springSecurityFilterChain, which is a DelegatingFilterProxy class. You can consider DelegatingFilterProxy as a proxy to all SecurityFilterChains that have been configured. DelegatingFilterProxy was added by AbstractSecurityWebApplicationInitializer at the startup as part of the autoconfiguration in SecurityFilterAutoConfiguration.java

When DelegatingFilterProxy is processed, it is converted into a VirtualFilterChain that has the same behavior as ApplicationFilterChain, but is unique to spring security.
VirtualFilterChain.

UsernamePasswordAuthenticationFilter

One of the filter here is UsernamePasswordAuthenticationFilter class in org.springframework.security.web.authentication that handles the form login authentication. UsernamePasswordAuthenticationFilter is added in FormLoginConfigurer.java in http.formLogin(withDefaults())

UsernamePasswordAuthenticationFilter does the following thing:

  1. Retrieve username and password from Http request and construct an Authentication object.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
    throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
    }

    • UsernamePasswordAuthenticationToken implicitly extends Authentication.class.
  2. Pass the Authentication object to the ProviderManager in org.springframework.security.authentication. Then the ProviderManager will choose one of the providers, in this case is the DaoAuthenticationProvider.java in org.springframework.security.authentication.dao.DaoAuthenticationProvider.
  3. DaoAuthenticationProvider will call retrieveUser() (Line#10) that uses your loadUserByUsername() method to retrieve an UserDetails object. Then DaoAuthenticationProvider will use the 3 three checking methods from AbstractUserDetailsAuthenticationProvider:
    • preAuthenticationChecks() (Line#22): checks for isAccountNonLocked(), isEnabled() and isAccountNonExpired().
    • additionalAuthenticationChecks() (Line#23): real check happened here. For DaoAuthenticationProvider, it checks for password matches.
    • postAuthenticationChecks() (Line#36) : checks for isCredentialsNonExpired().
      After all these checks, an AuthenticationToken object is created and returned at the end.
      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
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
      String username = determineUsername(authentication);
      boolean cacheWasUsed = true;
      UserDetails user = this.userCache.getUserFromCache(username);
      if (user == null) {
      cacheWasUsed = false;
      try {
      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException ex) {
      this.logger.debug("Failed to find user '" + username + "'");
      if (!this.hideUserNotFoundExceptions) {
      throw ex;
      }
      throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }
      Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
      }
      try {
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
      throw ex;
      }
      // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      cacheWasUsed = false;
      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
      }
      this.postAuthenticationChecks.check(user);
      if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
      }
      Object principalToReturn = user;
      if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
      }
      return createSuccessAuthentication(principalToReturn, authentication, user);
      }
  4. Once the AuthenticationToken is returned, there will be more operations such as cache or session storage etc. But I want to wail in the future to dig through more.

Other Filters

After UsernamePasswordAuthenticationFilter, we have DefaultLoginPageGeneratingFilter and DefaultLogoutPageGeneratingFilter. By the name, these two filters generate Spring provided login.html and logout.html when you try to access /login or /logout.

The last few filters deal with

Conclusion

I am starting to understand a bit how security filters work in spring security. I hope let’s get more and more hands-on the code next post.