From Spring Security in Action by Laurentiu Spilca

This article delves into five awkward things that Spring Security does, which might be giving you trouble in your projects.


Take 37% off Spring Security in Action. Just enter fccspilca into the discount code box at checkout at manning.com.


Spring Security has quickly become the de facto solution for implementing application-level security in apps developed with Spring Framework. And I don’t think I need to mention anymore that Spring is the first choice today for the development of enterprise applications in the Java ecosystem. While security in the software system is a must, Spring Security has become a necessary tool for every Spring developer. But Spring Security is known not to have a smooth learning curve and we can argue that its developers weren’t too attentive to the details that might have made it easy to work with. Here are five awkward things about Spring Security that actually make sense:

No. 5 – Unauthenticated vs. Failed authentication

Authentication is the process in which the application recognizes who you are. The basic way of authentication is providing a username and a password known by the system. Whenever calling a functionality of the application, the app has to first find out who you are. In most cases, authorization follows authentication. This means that the application knows who you are, so it knows what you are allowed to do (figure 1). The application then authorizes you if you are permitted to access certain functionality or data.


Figure 1. After the application authenticates the user, it decides whether to allow the request.


In Spring Security, you can design endpoints to be accessed by anyone. That is, anyone, authenticated or not, can make requests for that endpoint. You can make these requests without providing a username and a password. In this case, Spring Security won’t do the authentication anymore. If you, however, provide a username and a password, Spring Security will evaluate them in the authentication process. If they are wrong (not known by the system), the authentication will fail, and the response status will be 401 Unauthorized (figure 2).


Figure 2. If you send credentials in the request, the app will evaluate them. If they are wrong, the request is not forwarded to the authorization filter, even if the authorization configuration would allow the call.


To be more precise, let’s take an example. You have created an endpoint /hola in your application and configured it to be authorized for anyone using the permitAll() method in the configuration class. If you call the /hola endpoint without a user, it will return the body “Hola!” as expected, and the response status will be 200 OK.

 
 curl http://localhost:8080/hola
 Hola!
  

But if you call the endpoint with non-valid credentials, the status of the response is 401 Unauthorized. In the next call, I use an invalid password.

 
 curl -u john:abcde http://localhost:8080/hola
 {
     "status":401,
     "error":"Unauthorized",
     "message":"Unauthorized",
     "path":"/hola"
 }
  

Some could argue here that using permitAll() should make the application always allow any request from anybody. This behavior of the framework might look strange. Why does it care about username and password if it would approve the request without the credentials? But it makes sense, as the framework evaluates any username and password if you provide them in the request. The application always does authentication before authorization.

No. 4 – Method names in the UserDetails interface

In Spring Security, the UserDetails interface describes the user in the way the framework expects you to implement it. Nothing strange yet. It declares obvious methods like getUsername()—meant to return the username, getPassword(), and getAuthorities()—which  are related to privileges evaluated during authorization. But the contract defines four other methods that could sound strange (at least when you see them the first time):

  • boolean isAccountNonExpired();
  • boolean isAccountNonLocked();
  • boolean isCredentialsNonExpired();
  • boolean isEnabled();

The isEnabled() sounds good. But what’s with all the other three double negations? I know a few people that could get a heart-attack seeing this kind of naming at a code-review! Why isAccountNonExpired()? Couldn’t that simply be isAccountExpired()? In the end, it’s still your decision if you need to return true or false when you implement it. Well, it seems this naming convention was chosen on purpose. The human brain is made to understand false as negativity and true as positivity subconsciously. All four methods are made such that they all should return false for negation and true for the positive scenario. This chosen approach of naming the methods is, of course, still debatable. While it makes sense from one point of view, it is still a pain for a beginner for whom the double negation could create confusion.

No. 3 – ANT vs. MVC matchers

To implement the endpoint authorization rules with Spring Security, we use matcher methods. The framework offers three kinds of matcher methods:

  • ANT matchers – with them, you use ANT path notation to refer to paths.
  • MVC matchers – borrow syntax from ANT to refer to paths.
  • Regex matchers – with them, you use regular expressions to refer to paths.

I’ve often seen ANT and MVC matchers creating confusion among developers. Generally, the confusion happens because both matcher methods use in the end ANT syntax to refer to paths. Then what’s the difference between the two, and when should we use them? MVC matchers are designed to apply precisely to what the Spring MVC @RequestMapping and related annotations understand, as we will see with the following example.

Let’s assume you have an endpoint that you configured in one of the controller classes of your application, as presented in the next code snippet:

 
 @RestController
 public class HelloController {
  
     @GetMapping("/hello")
     public String hello() {
         return "Hello!";
     }
  
 }
  

You would then use ANT matchers to secure the endpoint, as presented in the next code snippet.

 
 @Configuration
 public class ProjectConfig extends WebSecurityConfigurerAdapter {
  
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.httpBasic();
  
         http.authorizeRequests()
                 .antMatchers( "/hello").authenticated();
     }
 }
  

With this configuration, you might be stupefied to observe that authorization is configured correctly for the /hello endpoint, but not also for the /hello/ path. By this, I mean that you need to authenticate to access the /hello endpoint, but you need no credentials for the /hello/ path which, obviously, leads to the same endpoint. This is huge! Developers not knowing this and using ANT matchers could potentially have unprotected endpoints. Just replace the antMatchers() method with mvcMatchers(), as presented in the next code snippet.

 
 @Configuration
 public class ProjectConfig extends WebSecurityConfigurerAdapter {
  
     @Override
     protected void configure(HttpSecurity http) throws Exception {
         http.httpBasic();
  
         http.authorizeRequests()
                 .mvcMatchers( "/hello").authenticated();
     }
 }
  

If you test the change now, you’ll see that both /hello and /hello/ are protected as you expected from the beginning. Being that in Spring MVC, both are leading to the same endpoint, when you use MVC matchers, Spring knows and protects both paths. For this reason, I always recommend MVC matchers above ANT matchers when applying authorization configurations with Spring Security.

No. 2 – The auto-configuration of the default PasswordEncoder component

If you work with Spring Security and Spring Boot, which is quite common nowadays, you might have observed how the auto-configuration for Spring Security is working. By default, Spring Boot configures a UserDetailsService, which manages one user in-memory. The username of this default user is “user”, a UUID is generated for the password and the credentials are printed in the console when you start the application. They’ll look similar to the next code snippet.

 
 Using generated security password: 93a01cf0-794b-4b98-86ef-54860f36f7f3
  

The first thing you do is to override this configuration, of course. So this is where I’ve seen a lot of developers get confused when starting with Spring Security and Spring Boot. You’ll define your custom UserDetailsService, and when trying to start your application, you’ll find in the console an error message. The message is complaining about the absence of a PasswordEncoder.

 
 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
       at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:244) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]
  

Awkward or not, the PasswordEncoder is a mandatory component in the HTTP Basic authentication flow (figure 3). But where was it until now? Why is the application complaining now?


Figure 3. The basic authentication flow.


The UserDetailsServiceAutoConfiguration class takes care of the auto-configuration for both the default UserDetailsService and PasswordEncoder. When you define a custom UserDetailsService, neither one of these two components is configured anymore by Spring Boot. The reason why they are configured all together is because of their strong relationship with each other. The UserDetailsService takes care of retrieving the user details during authentication. The PasswordEncoder is afterward used to check whether the password provided with the call matches the one known by the system. So a PasswordEncoder doesn’t make sense without a UserDetailsService that takes care of obtaining the user from the system. Neither a UserDetailsService makes more sense without a PasswordEncoder.

So, in the end, it is normal for both of them to be specified together. If you leave the default configuration, you’ll use a PasswordEncoder implementation which works with the provided UserDetailsService. If you provide a custom UserDetailsService, you should make sure to also provide a proper implementation of the PasswordEncoder.

No. 1 – The passwordEncoder() method in the User builder class

In frameworks and libraries, we might sometimes find names that are unfortunately given in such a way that they create confusion. This is also the case with the PasswordEncoder interface and the passwordEncoder() method of the User builder class. Again, this is a point that I have seen create confusion among developers. The names of these two, however, are perhaps not so poorly chosen as it might seem at first glance.

The PasswordEncoder is the interface describing the contract for the implementation of one of the components used in the authentication process.

Two methods define the responsibility of this component:

  • encode(String rawPassword), whose purpose is to apply encoding for a password given as parameter.
  • matches(String encoding, String rawPassword), whose goal is to check whether a given password matches an encoding (where the encoding is defined as implemented by the encode() method of the same interface).

The passwordEncoder() method of the User builder class only refers to the encoding of the password. For this reason, the parameter of the passwordEncoder() method is a Function. It receives the encoding function to be applied to the password for an instance of a user created with the builder class. In many cases, it can actually refer directly to the encode() method of the configured PasswordEncoder component.

Both names, taken individually, correctly describe the elements they define. But when first learning them, they could confuse you. One could argue that this negatively impacts the learning curve of the framework, which I would find hard to refute.

Conclusion

No framework or library is perfect, and there is endless material for debate regarding which is better in a given curcumstance. Spring Security does a great job of minimizing the time needed to bake application-level security into your system. It is an excellent result of the work of a vast community continuously acting in the improvement of Spring Framework. It just takes a bit of effort to learn how to use it well.

If you want to learn more about the book, go check it out on our browser-based liveBook reader here.