15 Spring Security Common Errors

15 Spring Security Common Errors #

Hello, I’m Fu Jian. In the previous lessons, we learned about request parsing and filter usage in Spring Web development. In this lesson, we will continue with the application of Spring Security.

In fact, in Spring, most of the handling of Security is done with the help of filters. It is not too difficult to use roughly, but Security itself is a very large topic, so there are naturally many errors that can be encountered. Fortunately, there are so many applications and developers using Spring Security that, to this day, there are not many obvious pitfalls.

In today’s class, I will quickly teach you about two typical errors. I believe that once you have mastered them, you don’t need to worry too much about the pitfalls of Spring Security. However, it should be noted that there are countless types of authorization, and here, in order to avoid getting tangled up in business logic implementation, I will explain the examples directly based on the default Spring Security implementation in Spring Boot. Now, let’s officially begin the course.

Case 1: Forgetting PasswordEncoder #

When we first try to use Spring Security, we often forget to define a PasswordEncoder. This was allowed in older versions of Spring Security, but in newer versions a PasswordEncoder must be provided. Let’s start with a negative example to understand the issue:

First, we enable Spring Security directly in our Spring Boot project by adding the following dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

After adding this dependency, Spring Security will take effect immediately. Then we configure the security policy as shown below:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("pass").roles("ADMIN");
    }

    // Configure access permissions for URLs
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and().csrf().disable();
    }
}

Here, we intentionally “comment out” the definition of the PasswordEncoder bean. Then, we define a SpringApplication to start the service and we will see the application starts successfully:

INFO 8628 — [ restartedMain] c.s.p.web.security.example1.Application : Started Application in 3.637 seconds (JVM running for 4.499)

However, when we send a request (e.g. http://localhost:8080/admin), we will encounter an error: java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”, with the following error stack trace:

So, if we do not follow the latest Spring Security tutorial, it is easy to forget about the PasswordEncoder. But why does the absence of it cause an error? And what is the purpose of the PasswordEncoder? Let’s analyze it further.

Analysis of the Case #

Let’s reflect on why a PasswordEncoder is necessary. In fact, it falls under the realm of security protection.

Suppose we do not have such a mechanism. How would we determine if a user’s input password matches the password stored in memory or in the database? If we simply compare the passwords as strings, the stored password must be stored in plaintext, which poses a risk of password leakage.

On the other hand, for security purposes, we typically store passwords in encrypted form. So when a user enters a password, we cannot simply compare the two strings. We need to compare the input password with the stored password based on the encryption algorithm used to store it. This is why we need a PasswordEncoder to meet this requirement. Now you understand the purpose of defining a custom PasswordEncoder.

Let’s take a closer look at the two key methods of the PasswordEncoder, encode() and matches(), and you will understand their roles.

Consider this: if we provide a default PasswordEncoder and integrate it into Spring Security, it could potentially hide errors. Therefore, it is more appropriate to require an explicit definition.

Now let’s see how the exception “no PasswordEncoder” is thrown in the source code. When we do not specify a PasswordEncoder to start our case program, we actually specify a default one, as can be seen in the constructor DaoAuthenticationProvider:

public DaoAuthenticationProvider() {
    setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}

Let’s take a look at the implementation of PasswordEncoderFactories.createDelegatingPasswordEncoder():

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", new Pbkdf2PasswordEncoder());
    encoders.put("scrypt", new SCryptPasswordEncoder());
    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", new Argon2PasswordEncoder());

    return new DelegatingPasswordEncoder(encodingId, encoders);
}

Let’s take a different perspective and see what this DelegatingPasswordEncoder looks like:

From the figure above, we can see that it actually integrates multiple built-in PasswordEncoders together.

When we verify users, we will use the following code to match, refer to DelegatingPasswordEncoder#matches:

private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    if (rawPassword == null && prefixEncodedPassword == null) {
        return true;
    }
    String id = extractId(prefixEncodedPassword);
    PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
    if (delegate == null) {
        return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
    }
    String encodedPassword = extractEncodedPassword(prefixEncodedPassword);

    return delegate.matches(rawPassword, encodedPassword);
}

private String extractId(String prefixEncodedPassword) {
    if (prefixEncodedPassword == null) {
        return null;
    }
    //{
    int start = prefixEncodedPassword.indexOf(PREFIX);
    if (start != 0) {
        return null;
    }
    //}
    int end = prefixEncodedPassword.indexOf(SUFFIX, start);
    if (end < 0) {
        return null;
    }
    return prefixEncodedPassword.substring(start + 1, end);
}

As we can see, assuming our prefixEncodedPassword contains an id, we can find the appropriate Encoder based on the id in DelegatingPasswordEncoder’s idToPasswordEncoder map; assuming there is no id, the default UnmappedIdPasswordEncoder is used. Let’s take a look at its implementation:

private class UnmappedIdPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        throw new UnsupportedOperationException("encode is not supported");
    }

    @Override
    public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
        String id = extractId(prefixEncodedPassword);
        throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
    }
}

From the above code, we can see that the “no PasswordEncoder mapped for the id ’null’” exception is thrown by UnmappedIdPasswordEncoder. So what is this prefixEncodedPassword that may contain an id? In fact, it is the stored password, specified by the password() method in the following line of our case:

auth.inMemoryAuthentication()
    .withUser("admin").password("pass").roles("ADMIN");

Let’s test it by modifying the code above and specifying an encryption method for the password, and see if the previous exception still exists:

auth.inMemoryAuthentication()
    .withUser("admin").password("{MD5}pass").roles("ADMIN");

At this point, when you run the program in debug mode, you will find that there is already an id, and the appropriate PasswordEncoder has been retrieved.

Speaking of this, I believe you already know the ins and outs of the problem. The root cause of the problem is still that we need a PasswordEncoder, and the current case did not specify it for us.

Problem Fix #

After analysis, you must know how to solve this problem, which is nothing more than customizing a PasswordEncoder. For specific correction code, you can refer to the previous code provided, and it will not be repeated here.

In addition, through case analysis, I believe you have also thought of another way to solve the problem, which is to make changes to the stored password. Specifically, in our case, you can use the following correction method:

auth.inMemoryAuthentication()
    .withUser("admin").password("{noop}pass").roles("ADMIN");

Then locating to this method is actually equivalent to specifying NoOpPasswordEncoder as the PasswordEncoder, and its implementation is as follows:

public final class NoOpPasswordEncoder implements PasswordEncoder {
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.toString().equals(encodedPassword);
    }

    // Omitted some non-critical code
}

However, this fix is a bit cumbersome, after all, it is not appropriate to add a prefix to each password. Therefore, from a comprehensive comparison, the first fix is more general. Of course, if your requirement is different encryption for different users, perhaps this method is also good.

Case 2: ROLE_ Prefix and Roles #

Let’s take a look at another example in Spring Security regarding role-based authorization. Should we add the ROLE_ prefix or not? However, this time we need to provide a slightly more complex functionality, which is simulating role-based control during authorization. So we need to improve the example. First, I’ll provide an interface that requires management of the following operation permissions:

@RestController
public class HelloWorldController {
    @RequestMapping(path = "admin", method = RequestMethod.GET)
    public String admin(){
         return "admin operation";
    };

Then, we use the default built-in authorization of Spring Security to create an authorization configuration class:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Same as Case 1, omitted here
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("fujian").password("pass").roles("USER")
            .and()
            .withUser("admin1").password("pass").roles("ADMIN")
            .and()
            .withUser(new UserDetails() {
                @Override
                public Collection<? extends GrantedAuthority> getAuthorities() {
                    return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));

                }
                // Omitted other non-critical "implementation" methods
                public String getUsername() {
                    return "admin2";
                }

            });
    }

    // Configure access permissions for URLs
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
              .antMatchers("/admin/**").hasRole("ADMIN")
              .anyRequest().authenticated()
              .and()
              .formLogin().loginProcessingUrl("/login").permitAll()
              .and().csrf().disable();
    }
}

With the above code, we have added 3 users:

  1. User ‘fujian’: role USER
  2. User ‘admin1’: role ADMIN
  3. User ‘admin2’: role ADMIN

Then, when we access our interface http://localhost:8080/admin from a browser and log in with the above 3 users, we will find that user ‘admin1’ can log in while ‘admin2’, even though they have the same role, cannot log in and the following error is displayed:

How can we understand this phenomenon?

Case Analysis #

To understand the reason behind this case, we actually need a deeper understanding of the ROLE_ prefix in Spring Security. However, before that, you may not expect that the culprit causing the error in this case is the ROLE_ prefix. So we need to find some clues first.

By comparing the addition of users ‘admin1’ and ‘admin2’, you will find that they are just two different styles of adding built-in users. But why does the former work correctly while the latter does not? The root cause lies in the style of setting the role. Please refer to the following two key snippets of code:

// Adding admin1
.withUser("admin").password("pass").roles("ADMIN")

// Adding admin2
.withUser(new UserDetails() {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));
    } 
    @Override
    public String getUsername() {
        return "admin2";
    }
    // Omitted other non-critical code
});

By looking at these two ways of adding users, you will find that they are just two different styles, so the final code for constructing the users must be the same. Let’s first take a look at how admin1 is added and how the role is handled in the end (refer to User.UserBuilder#roles):

public UserBuilder roles(String... roles) {
   List<GrantedAuthority> authorities = new ArrayList<>(
         roles.length);
   for (String role : roles) {
      Assert.isTrue(!role.startsWith("ROLE_"), () -> role
            + " cannot start with ROLE_ (it is automatically added)");
      // Add "ROLE_" prefix
      authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
   }
   return authorities(authorities);
}

public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
   this.authorities = new ArrayList<>(authorities);
   return this;

Looking at these two ways of adding users, you will find that they are just two different styles, so the final code for constructing the users must be the same. Let’s first look at how admin1 is added and how the role is handled in the end (refer to User.UserBuilder#roles):

public UserBuilder roles(String... roles) {
   List<GrantedAuthority> authorities = new ArrayList<>(
         roles.length);
   for (String role : roles) {
      Assert.isTrue(!role.startsWith("ROLE_"), () -> role
            + " cannot start with ROLE_ (it is automatically added)");
      // Add "ROLE_" prefix
      authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
   }
   return authorities(authorities);
}

In the code snippet above, we can see that when adding roles, the method roles checks whether each role starts with the “ROLE_” prefix. If it doesn’t, it automatically adds the prefix. This is why the role is automatically set to “ROLE_ADMIN” for user ‘admin1’.

On the other hand, for the user ‘admin2’, we provided a custom implementation of UserDetails and returned a SimpleGrantedAuthority with the role name “ADMIN”. However, we did not add the “ROLE_” prefix. This is why the role for user ‘admin2’ is set to “ADMIN” instead of “ROLE_ADMIN”.

This is the reason why user ‘admin1’ can log in successfully while user ‘admin2’ cannot. When Spring Security checks the role, it expects the role to start with the “ROLE_” prefix by default.

}

It can be seen that when admin1 adds the ADMIN role, what is actually added is ROLE_ADMIN. But let's take a look at the role setting for admin2. The final setting method is actually User#withUserDetails:

```java
public static UserBuilder withUserDetails(UserDetails userDetails) {
    return withUsername(userDetails.getUsername())
        // omitted non-essential code
        .authorities(userDetails.getAuthorities())
        .credentialsExpired(!userDetails.isCredentialsNonExpired())
        .disabled(!userDetails.isEnabled());
}

public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = new ArrayList<>(authorities);
    return this;
}

So, for admin2, the final role that is set is ADMIN.

From this we can draw a conclusion: when the same role (ADMIN) is set using the two methods mentioned above, the stored role is different - ROLE_ADMIN and ADMIN, respectively. But why can only users with the role ROLE_ADMIN pass the authorization? Here, let’s take a look at the authorization call stack through debugging, as shown in the screenshot:

Authorization call stack

For the code in question, the authorization is ultimately completed by the UsernamePasswordAuthenticationFilter. And from the call stack information, we can roughly see that the key to the authorization is to look up the user and then validate the authority. The method for looking up the user can be found in InMemoryUserDetailsManager#loadUserByUsername, which looks up the added users based on their usernames:

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    UserDetails user = users.get(username.toLowerCase());

    if (user == null) {
        throw new UsernameNotFoundException(username);
    }

    return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
            user.isAccountNonExpired(), user.isCredentialsNonExpired(),
            user.isAccountNonLocked(), user.getAuthorities());
}

After completing checks for expired accounts and locked accounts, we convert this user into the following token (UsernamePasswordAuthenticationToken) for subsequent use, with the key information as follows:

Token information

When determining the role, we use the parent class method AbstractAuthenticationToken#getAuthorities to obtain ADMIN from the screenshot above. The key method used to determine if a user has a specific role is SecurityExpressionRoot#hasAnyAuthorityName:

private boolean hasAnyAuthorityName(String prefix, String... roles) {
    // obtain "role" through AbstractAuthenticationToken#getAuthorities
    Set<String> roleSet = getAuthoritySet();

    for (String role : roles) {
        String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
        if (roleSet.contains(defaultedRole)) {
            return true;
        }
    }

    return false;
}

private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
    if (role == null) {
        return role;
    }
    if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
        return role;
    }
    if (role.startsWith(defaultRolePrefix)) {
        return role;
    }
    return defaultRolePrefix + role;
}

In the above code, prefix is ROLE_ (default value, i.e., SecurityExpressionRoot#defaultRolePrefix), Roles is the role to match (ROLE_ADMIN), and the resulting defaultedRole is ROLE_ADMIN. Our role set is obtained from the UsernamePasswordAuthenticationToken as ADMIN, so the final result of the check is false.

This result is then passed to the upper layer to determine whether or not to grant authorization, as can be seen in WebExpressionVoter#vote:

public int vote(Authentication authentication, FilterInvocation fi,
        Collection<ConfigAttribute> attributes) {
    // omitted non-essential code
    return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
            : ACCESS_DENIED;
}

Clearly, when the result of the check for a specific role (expression hasRole('ROLE_ADMIN')) is false, the return value is ACCESS_DENIED.

Fixing the Issue #

With the analysis of this case using the source code, it can be seen that the ROLE_ prefix is very important in Spring Security. To solve this problem, it is very straightforward: simply add the ROLE_ prefix when adding admin2’s role:

// Adding admin2
.withUser(new UserDetails() {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    @Override
    public String getUsername() {
        return "admin2";
    }
    // omitted other non-essential code
})

Referring to the code above, we added the prefix to the role. After running the program again, the result matches the expectation.

Upon reflection on this case, we can conclude: sometimes different APIs provide different ways to set roles, but we must pay attention to whether or not to add the ROLE_ prefix. As for how to determine this, I don’t have a better solution, so I can only rely on experience or reviewing the source code to verify it.

## Key Review

Finally, let's review the key points mentioned in the course.

1. PasswordEncoder

In the new version of Spring Security, you must remember to specify a PasswordEncoder, because for security reasons, we definitely need to encrypt passwords. As for how to specify it, there are actually multiple ways. The common way is to customize a Bean of type PasswordEncoder. Another less common way is to specify the encryption method by adding a prefix when storing the password. For example, the original password is password123, and after adding a prefix, it may become {MD5}password123. We can adopt different solutions according to our needs.

1. Role

When using role-related authorization functions, you must pay attention to whether the role has a prefix of ROLE_.

Although Spring has tried to add prefixes to many role settings, there are still many interfaces where roles can be set arbitrarily. So sometimes if you set roles without realizing this issue, the role control may not take effect during authorization checking. From another perspective, when your role setting fails, you must check if you forgot to add the prefix.

Above are the key points of this class. I hope you have gained something from it.
## Thought-provoking question

From our study of Case 1, we learned that when Spring Security is enabled in Spring Boot, accessing an API that requires authorization will automatically redirect to the following login page. Do you know how this page is generated?

![](../images/ed6c2836fb09418fa66771ae7c73b3c3.jpg)

We look forward to your thoughts in the comments section!