Spring Security Guide. Part 4: Out of the box implementations — HTTP Basic with UserDetailsService
Out-of-the-box implementations
Now, when we implemented the authentication part of the Spring Security mechanism in the most straightforward way (“Part 2: Simple authentication with Opaque token” and “Part 3: Simple authentication with JWT token”), let’s see what options Spring Security offers out of the box to simplify or enhance the authentication mechanism setup.
There are two main things that you can configure while implementing security based on out-of-the-box tooling:
- Which type of out-of-the-box authentication mechanism to enable (e.g. “form login”. “http basic”, etc)
- Specific details on how to authenticate the requests: parts that are not boilerplate code and do change from project to project (usually, by providing our own implementation of AuthenticationManager, AuthenticationProvider, UserDetailsService, etc).
As you will learn later: they are composable, meaning you may use “form login” + “custom UserDetailsService” or “form login” + “custom AuthenticationProvider”, or “http basic” + “custom AuthenticationProvider”, etc.
Two types of out-of-the-box authentication are available out-of-the-box: “form login” and “http basic”. Other types are available but require additional dependencies to be included (starters) (e.g. OAuth, SAML, X509, etc). The mentioned authentication mechanisms can be enabled simply by the invocation of the corresponding methods on the HttpSecurity DSL: “.formLogin()” or “httpBasic()”.
Enabling some of the out-of-the-box authentication mechanisms results in the registration of specific filters (by the corresponding configurers applied; we will learn about the configurers later in the series):
- UsernamePasswordAuthenticationFilter registered by the FormLoginConfigurer for the “.formLogin()” case
- BasicAuthenticationFilter registered by the HttpBasicConfigurer for the “.httpBasic()” case
Those filters use AuthenticationManager, which you can override by the corresponding DSL method (.authenticationManager(…)).
Also, you can define your own authentication providers by calling (.authenticationProvider(…)) on DSL [you can register many]: in this case, Spring Security will register ProviderManager implementation of AuthenticationManager under the hoods.
Another option is to register UserDetailsService implementation (by calling .userDetailsService(…) on DSL): in this case, Spring Security will register ProviderManager implementation of AuthenticationManager and also DaoAuthenticationProvider implementation of AuthenticationProvider, which, in turn, uses UserDetailsService internally.
Lastly, you can go even more into using out-of-the-box components of Spring Security and register out-of-the-box implementation of UserDetailsService: InMemoryUserDetailsManager for in-memory implementation or JdbcUserDetailsManager for basic SQL-database-based implementation.
HTTP basic with custom UserDetailsService
Source code: https://github.com/Igor-Polatajko/spring-boot-security-web/tree/http-basic-user-details-service
Now let’s consider HTTP basic authentication mechanism setup with custom UserDetailsService implementation.
A bit of theory on what is HTTP basic authentication scheme: in simple words, it is the type of authentication when the user sends his credentials (login and password) with each request. The format is the following:
Basic base64(<username>:<password>)
The credentials are simply separated by double-dot and base 64 encoded. As part of the auth scheme details “Basic” keyword would be included before the credentials. The resulting string is transferred in the “Authorization” header. Obviously, this type of authentication does not need any “login” endpoint, as the original user credentials are carried out with each request. This fact of transferring credentials with each request is also considered to be the biggest drawback with this type of authentication. Anyways, let’s see how it can be implemented with Spring Security, using out-of-the-box tooling, only providing a custom implementation of the UserDetailsService interface to specify how the user should be loaded by its username.
What will be implemented is the following (grey — happens under the hoods of Spring Security; yellow — elements, that have to be implemented by us):
Step-by-step description:
Step 1: Creating a class to represent authenticated user data.
We need to create a class to represent authenticated principal. When we rely on out-of-the-box implementation of authentication and want to customize the UserDetailsService, we need to conform to the contract of UserDetailsService: “loadUserByUsername(…)” method returns an instance of UserDetails, which means, now our AuthUser has to implement this interface.
package com.ihorpolataiko.springbootsecurityweb.security.user;
import com.ihorpolataiko.springbootsecurityweb.common.Role;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
// Now our user has to implement UserDetails, because that is required by UserDetailsService, which
// is used by BasicAuthenticationFilter under the hoods of Spring Security
// (BasicAuthenticationFilter -> ProviderManager -> DaoAuthenticationProvider -> UserDetailsService)
public record AuthUser(String userId, List<Role> roles, String passwordHash)
implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream().map(role -> new SimpleGrantedAuthority(role.name())).toList();
}
@Override
public String getPassword() {
return passwordHash;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}Step 2: Custom UserService implementation
Get Ihor Polataiko’s stories in your inbox
Join Medium for free to get updates from this writer.
After we created the class to represent authenticated user data, let’s implement the class, that will be responsible for loading user data and creating the instance of AuthUser to be later used by the Spring Security framework in “under the hood” implementation. The class we need to create is the UserDetailsService interface implementation.
package com.ihorpolataiko.springbootsecurityweb.security;
import com.ihorpolataiko.springbootsecurityweb.dto.user.UserResponseWithCredentials;
import com.ihorpolataiko.springbootsecurityweb.security.user.AuthUser;
import com.ihorpolataiko.springbootsecurityweb.service.UserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
public UserDetailsServiceImpl(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserResponseWithCredentials userCredentialsByUsername =
userService.getUserCredentialsByUsername(username);
return new AuthUser(
userCredentialsByUsername.userResponse().id(),
userCredentialsByUsername.userResponse().roles(),
userCredentialsByUsername.passwordHash());
}
}As we can see, this class does some logic (can be pretty much any logic: fetching a user from a database, calling a 3rd party service, etc) to fetch the required data about the user by user’s username and then constructs AuthUser object.
Even though we are able to implement the UserDetailsService interface directly by UserService, let’s separate it from our UserService to provide additional decoupling, so we define a new class UserDetailsServiceImpl, which would use already available API from UserService.
Step 3: Security configuration.
The only thing that is yet left to do is the registration of properly configured SecurityFilterChain bean.
package com.ihorpolataiko.springbootsecurityweb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
@EnableMethodSecurity // allow to specify access via annotations
@Configuration
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final AccessDeniedHandler accessDeniedHandler;
public SecurityConfig(
UserDetailsService userDetailsService,
AuthenticationEntryPoint authenticationEntryPoint,
AccessDeniedHandler accessDeniedHandler) {
this.userDetailsService = userDetailsService;
this.authenticationEntryPoint = authenticationEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults())
.userDetailsService(userDetailsService)
.authorizeHttpRequests(
mather ->
mather
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/*",
"/v3/api-docs",
"/v3/api-docs/swagger-config")
.permitAll())
.authorizeHttpRequests(
matcher ->
matcher
// method security will be evaluated after DSL configs,
// so we have to define public paths upfront
.requestMatchers(HttpMethod.POST, "/api/users")
.permitAll())
.authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(
customizer ->
customizer
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint));
return http.build();
}
}We need to call .httpBasic(…) method on HttpSecurity DSL which will result in the registration of all the required components including BasicAuthenticationFilter (by applying a dedicated configurer). We will use default settings, so we pass “Customizer.withDefaults()” as a parameter of this method.
Then we register the UserServiceDetails implementation by calling “.userDetailsService(…)” method: by applying a dedicated configurer, under the hoods, it will result also in the registration of ProviderManager implementation of AuthenticationManager and DaoAuthenticationProvider implementation of AutheticationProvider.
Remaining configuration: customized exception handling and authorization rules are similar to the “Part 2: Simple authentication with Opaque token”.
To avoid cyclomatic dependencies, we have to move the PasswordEncoder bean declaration to a separate configuration class.
package com.ihorpolataiko.springbootsecurityweb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfig {
// password encoder is required by the UserService, which would create circular dependency if we
// put this bean declaration to the SecurityConfig class (because, UserService is used by
// UserDetailsServiceImpl, which in turn is required by SecurityConfig). There are many possible
// ways to solve it, but let's just put this bean declaration into another class
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}Also, we need to update SwaggerConfiguration in the OpenApiConfig class, so we can call the APIs from Swagger using the Basic authentication scheme.
package com.ihorpolataiko.springbootsecurityweb.config;
import com.ihorpolataiko.springbootsecurityweb.common.OpenApiConstants;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
name = OpenApiConstants.BASIC_SECURITY_REQUIREMENT,
type = SecuritySchemeType.HTTP,
scheme = "basic")
public class OpenApiConfig {}Whenever you use a “Basic” authentication scheme, you have to remember, that (by default) user credentials evaluations will be executed for each request (UserDetailsService/AuthenticationProvider/Authentication manager will be called each time the application receives a request [matching the particular SecurityFilterChain]) in contrast to, e.g. form login when evaluation will happen only on the login and all the subsequent requests will be authenticated based on the “session cookie” (completely under the hoods of the framework).
There is, however, a way to rely on the session cookie for authorizing upcoming requests after the HTTP Basic authentication happened for the first request. In this case, user credentials evaluations will be executed only for the first request. To achieve it, we have to override the default SecurityContextRepository implementation leveraged by BasicAuthenticationFilter (which is RequestAttributeSecurityContextRepository) and set it to HttpSessionSecurityContextRepository instead:
http.httpBasic(
configurer ->
configurer.securityContextRepository(new HttpSessionSecurityContextRepository()))HTTP basic with InMemory UserDetailsService
Source code: https://github.com/Igor-Polatajko/spring-boot-security-web/tree/http-basic-inmemory
This example will aim to show, that we can leverage Spring Security out-of-the-box implementations to an even higher degree. SpringSecurity provides us with two (there are more, e.g. CachingUserDetailsService, but those are rather proxies and not stand-alone implementations) out-of-the-box implementations of UserDetailsService: InMemoryUserDetailsManager (provides in-memory user store), JdbcUserDetailsManager (provides SQL-database-based implementation).
One of the main limitations is that we no longer can provide our own implementation of UserDetails for the UserDetailsService (and, subsequently DaoAuthenticationProvider) to use. We have to stick to whatever is used by InMemoryUserDetailsManager or JdbcUserDetailsManager, which is org.springframework.security.core.userdetails.User. Hence, when we use the “@AuthenticatedPrincipal” annotation in the controllers: this is the type, that we should expect. Usually, you would not want to use any of those implementations in production, but for the sake of the demo, let’s consider them as well.
As for the implementation, it all comes down to a SecurityFilterChain bean configuration.
package com.ihorpolataiko.springbootsecurityweb.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@EnableMethodSecurity // allow to specify access via annotations
@Configuration
public class SecurityConfig {
private final String adminUsername;
private final String adminPassword;
public SecurityConfig(
// let`s just reuse the values, that we defined for our admin through the properties
@Value("${admin.default.username}") String adminUsername,
@Value("${admin.default.password}") String adminPassword) {
this.adminUsername = adminUsername;
this.adminPassword = adminPassword;
}
// we autowire AuthenticationManagerBuilder and register out-of-the-box implementation of
// UserDetailsService there
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(adminUsername)
// we add prefix {noop}, because the password is not hashed (raw)
// also, we do not register password encoder, so default is used
.password("{noop}" + adminPassword)
.roles("ADMIN"); // role without "ROLE_" prefix
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests(
mather ->
mather
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/*",
"/v3/api-docs",
"/v3/api-docs/swagger-config")
.permitAll())
.authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated())
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}We can either instantiate (create the object of InMemoryUserDetailsManager or JdbcUserDetailsManager via “new”) and configure one of the out-of-the-box implementations of UserDetailsService ourselves and then register it via the “.userDetailsService()” method on HttpSecurity, or, in the way we do it in this example, leverage the builder bean called AuthenticationManagerBuilder, which can be injected (because it’s created and registered into Spring container by AuthenticationConfiguration under the hoods). AuthenticationManagerBuilder has methods “.inMemoryAuthentication()” and “.jdbcAuthentication()” to configure and register instances of InMemoryUserDetailsManager and JdbcUserDetailsManager respectively.
By the way, “.userDetailsService()” method on HttpSecurity also delegates to “.userDetailsService()” method on AuthenticationManagerBuilder bean.
All remaining SecurityFilterChain bean configuration should be pretty familiar.
➡️ Next part: “Part 5: Out of the box implementations — Form”
⬅️ Previous part: “Part 3: Simple authentication with JWT token”
↩️ First part: “Part 1: Introduction”







