Authentication is the process through which we confirm the identity of a user attempting to access our application. Typically, users are asked to enter their login credentials, including their username and password. If the information provided is correct, the system recognizes the identity as valid and grants access to the user.
As you may already be aware, when we add the Spring Security starter dependency to our application, Spring Security automatically applies an authentication process and generates a default user. However, in most cases, we require multiple users to access our application. In this topic, we will guide you through the process of configuring authentication in Spring Security. We will create a few hardcoded in-memory users step by step and then review the example in full.
Note that this topic covers Spring Security 6.1.0
User storage
In order to configure authentication in Spring Security, we need to create a configuration class that provides the necessary Spring beans.
Java
import org.springframework.context.annotation.Configuration;
@Configuration
public class SecurityConfig {
}Kotlin
import org.springframework.context.annotation.Configuration
@Configuration
class SecurityConfig {
}For our purposes, we will define a UserDetailsService bean to create hardcoded users. To achieve this, we can make use of the built-in User class provided by Spring Security, which allows us to store user details such as usernames, credentials, and other relevant information. Using this class, we can specify the login and password pair for one or more users. Next, we will create an instance of the InMemoryUserDetailsManager, which implements the UserDetailsService interface. This interface allows us to find a user by their username. The InMemoryUserDetailsManager serves as an in-memory storage and offers a variety of other methods to manage users, such as createUser, deleteUser, and changePassword, among others.
Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("user1")
.password("pass1")
.roles()
.build();
UserDetails user2 = User.withUsername("user2")
.password("pass2")
.roles()
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
}Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.UserDetailsManager
@Configuration
class SecurityConfig {
@Bean
fun userDetailsService(): UserDetailsService {
val user1 = User.withUsername("user1")
.password("pass1")
.roles()
.build()
val user2 = User.withUsername("user2")
.password("pass2")
.roles()
.build()
)
return InMemoryUserDetailsManager(users)
}
}It's worth noting that in addition to specifying the login and password, we have one additional method call, namely roles(). This method is used to define zero or more user roles. In our case, we aren't using any roles, but you'll learn about user roles in upcoming topics.
It's important to keep in mind that this approach is primarily useful for testing and providing examples. Typically, user information would be stored in a database. We will demonstrate how to store users in a database in upcoming topics.
To enable multiple hardcoded users, there is one more step we need to take. If we were to run a program with the implementation described above and try to access a resource within our application, we would see an error in the console after entering one of the correct login/password pairs. Specifically, the console log would contain the following entry:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"This indicates that the password encoder hasn't been specified. In the following section, we will explain what this means and how to address this issue.
Password encoders
For security reasons, passwords should never be stored in plain text and should be encoded instead. If passwords are stored in plain text within a database and someone gains access to that database – be it a hacker or another employee – they will be able to copy unencoded passwords, log in as a user, and perform account-related operations such as sending messages, transferring money, or blackmailing a real user. Storing passwords in an encoded format makes it much more difficult for someone to impersonate a user.
In Spring Security, password encoding is achieved through the implementation of the PasswordEncoder interface. This interface has two abstract methods: encode and matches. The encode method receives a raw password and returns an encoded version. This method is used before storing a password. The matches method receives a raw password and an encoded password and returns true if the passwords match, otherwise it returns false. This method is used by Spring Security during the authentication process to check if the input raw password matches the encoded password that is stored.
Spring Security requires developers to use a password encoder, otherwise the program won't work properly. In our case, even though we're not using a database to store user credentials and the passwords are visible, we still need to use a password encoder.
To get our program working, we need to encode a password before storing it and inform Spring Security of which encoder we used so that it can use the encoder during the authentication process. In this demo application, we can create users with a default password encoder or instantiate a desired implementation PasswordEncoder.
Java
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}Kotlin
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
@Bean
fun passwordEncoder(): PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()We can create an instance of DelegatingPasswordEncoder, which will invoke the appropriate concrete PasswordEncoder implementation (such as bcrypt, SHA-256, noop, etc.) depending on the password prefix. Alternatively, we can create an instance of the implementation of PasswordEncoder of our choice, such as BCryptPasswordEncoder. Then, we can encrypt the first user's password with the encoder:
Java
UserDetails user1 = User.withUsername("user1")
.password(this.passwordEncoder().encode("pass1"))
.roles()
.build();Kotlin
val user1 = User.withUsername("user1")
.password(passwordEncoder().encode("pass1"))
.roles()
.build()Alternatively, we can create the second user with a default password encoder:
Java
UserDetails user2 = User.withDefaultPasswordEncoder()
.username("user2")
.password("pass2")
.roles()
.build();Kotlin
val user2 = User.withDefaultPasswordEncoder()
.username("user2")
.password("pass2")
.roles()
.build()It's worth noting that the withDefaultPasswordEncoder() method is marked as deprecated to highlight that it is not safe for production usage, as the raw password is hardcoded and remains in memory. Under the hood, this method binds an instance of DelegatingPasswordEncoder to the user in the same way as we did for the first user.
Before looking at the full code example, let's take a look at another method that we might stumble upon when creating a user, as we've seen before. The method in question is: passwordEncoder(Function<String, String> encoder). This method takes an encoder, i.e. a Function that takes as input a string, our raw password, and should output the encoded password. Configuring the user as we see below will result in the same "There is no PasswordEncoder mapped" error we met before.
UserDetails user = User.withUsername("user")
.passwordEncoder(pwd -> pwd)
.password("secret")
.roles()
.build();Now let's examine the full code example and discuss what happens during authentication.
Putting pieces together
The full implementation looks like this:
Java
package org.hyperskill.hsspringtopics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("user1")
.password(this.passwordEncoder().encode("pass1"))
.roles()
.build();
UserDetails user2 = User
.withDefaultPasswordEncoder()
.username("user2")
.password("pass2")
.roles()
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.factory.PasswordEncoderFactories
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager
@Configuration
class SecurityConfig {
@Bean
fun userDetailsService(): UserDetailsService {
val user1 = User.withUsername("user1")
.password(passwordEncoder().encode("pass1"))
.roles()
.build()
val user2 = User.withDefaultPasswordEncoder()
.username("user2")
.password("pass2")
.roles()
.build()
return InMemoryUserDetailsManager(user1, user2)
}
@Bean
fun passwordEncoder(): PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
}It's important to note that since we're overriding the default configuration, the default user won't be created.
If we create and run a program with this implementation, we'll be able to access it using one of the valid login/password pairs, and form-based or HTTP basic authentication, which, as you already know, are enabled by default.
When a user tries to authenticate, Spring Security will search for a user with the specified login. If the user is found, the password encoder and its matches method will be used to check if the input raw password matches the stored encoded one. If everything is correct, the user is allowed to access the application, and authentication is completed.
It's worth noting that regardless of where user credentials are loaded from, the authentication process is similar. In the example above, we stored the users in memory, but typically there would be a database with user information and an endpoint responsible for user registration. This endpoint would populate the database with user information.
Furthermore, the default configuration related to form-based and HTTP basic authentication can be customized and configured as well.
HttpSecurity
To specify which authentication methods are allowed, such as form-based or HTTP basic, and how they are configured, we can create a SecurityFilterChain bean. In our bean definition method, we can inject an HttpSecurity builder and use it to customize an instance of DefaultSecurityFilterChain. Any incoming HTTP request will be processed by the filters in this chain, which will either grant or reject access to the requested resource.
The example below shows the configuration that is equivalent to the default one:
Java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(matcherRegistry -> matcherRegistry
.anyRequest().authenticated() // 1
)
.formLogin(Customizer.withDefaults()) // 2
.httpBasic(Customizer.withDefaults()) // 3
.build();
}Kotlin
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
.authorizeHttpRequests { matcherRegistry ->
matcherRegistry.anyRequest().authenticated()
}
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.build()To access any URL (
anyRequest()) on our app, a user needs to authenticate (authenticated()).Form-based authentication is enabled with default settings.
HTTP basic authentication is enabled with default settings.
This default configuration is why your application is immediately secure as soon as you add the Spring Security starter dependency.
When we create a SecurityFilterChain using the builder, we override the default configuration. For example, if we don't explicitly include HTTP Basic authentication, it will not be enabled. The same applies to form-based authentication, which will be disabled if we remove the formLogin method from the code above. We can also add more method calls to the method chain in the SecurityFilterChain builder to enable, disable, or modify the configuration of CSRF protection, security headers, session management, and more.
The HttpSecurity builder methods, such as formLogin and httpBasic accept a Customizer object that can be used to customize the login page's appearance for form-based authentication and to perform other tasks. The Customizer is a functional interface with a single abstract method that accepts the corresponding configurer object as a parameter. You can create a Customizer object using a lambda function, but in the example given, the Customizer.withDefaults() method is used, which creates a configurer that doesn't modify the default configuration.
Finally, it's important to note that authentication is not limited to logins and passwords. It's possible to implement fingerprint authentication or authentication via SMS, where the user has to enter a code sent to their phone via SMS as proof of their identity. Spring Security allows for configuring this functionality as well, but we won't be discussing it in this topic.
Conclusion
Throughout this topic, we've explored how to override the default authentication configuration and create one or more hardcoded in-memory users. We've also seen how to explicitly enable or disable form-based and HTTP basic authentication. It's important to note that the method we used to add hardcoded users is just one possible approach, and Spring Security offers more flexibility with other ways to achieve the same functionality.
By familiarizing yourself with these concepts and customizing your authentication configuration to fit your specific needs, you can improve the security of your Spring application and provide your users with a seamless and secure experience. Remember that authentication is just one aspect of application security, and it's important to consider other security measures such as authorization, input validation, and error handling.