Sitemap

Spring Security Guide. Part 5: Out of the box implementations — Form login

11 min readDec 4, 2024

--

Form login with custom UserDetailsService

Source code: https://github.com/Igor-Polatajko/spring-boot-security-web/tree/form-login-user-details-service

Press enter or click to view image in full size

In the previous Part, we tried to set up an HTTP basic security mechanism. In this one, let’s try another out-of-the-box authentication method provided by Spring Security called form login.

First of all, we have to implement server-side rendered multipage web application. For the sake of simplicity, only items-management functionality will be available in the web application, user management will not be.

In order to do this, we have to implement web controllers annotated with “@Controller” annotation, which handle only GET and POST requests (because those are only the two types, that browsers allow us to do without using JavaScript). GET requests will be rendering pages, POST requests (initiated from HTML forms) will be doing some processing and returning redirect to the URL of the page, that the user should see after the request is processed (Post/Redirect/Get pattern). All of these controllers can be found in the “controller.web” package, templates for the pages are located in the “resources/templates” folder. We would need to add Freemarker dependency to process the rendering of the pages (but you can use a template engine of your choice, e.g. Thymeleaf). Services (business logic) remain unchanged because adding the new “outbound adapter” [as per the terminology of the Hexagonal architecture], which is our new set of web controllers, should not bother them.

Almost all of the setup is pretty similar to the HTTP basic with custom UserDetailsService. Only the SecurityFilterChain bean configuration via HttpSecurity in the SecurityConfig class will be a bit different:

package com.ihorpolataiko.springbootsecurityweb.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.SecurityFilterChain;


@EnableMethodSecurity // allow to specify access via annotations
@Configuration
public class SecurityConfig {


private final UserDetailsService userDetailsService;


public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


http.formLogin(Customizer.withDefaults())
// provide user details service implementation
.userDetailsService(userDetailsService)
// allow public access to the home page
.authorizeHttpRequests(mather -> mather.requestMatchers("/").permitAll())
// deny requests to API for this example
.authorizeHttpRequests(mather -> mather.requestMatchers("/api/*").denyAll())
// deny requests to Swagger UI
.authorizeHttpRequests(
mather ->
mather
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/*",
"/v3/api-docs",
"/v3/api-docs/swagger-config")
.denyAll())
.authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated())
.exceptionHandling(customizer -> customizer.accessDeniedPage("/no-access"));


return http.build();
}
}

We call “.formLogin(…)” on HttpSecurity. As we do not want to provide any customization, we pass Customizer.withDefaults() as a parameter.

Then we register our custom UserDetailsService implementation.

After that, we configure authorization rules (evaluated by AuthorizationFilter).

We deny access to API endpoints for the sake of this demo.

Also, we will not be overriding the AuthenticationEntryPoint and AccessDeniedHandler as we are perfectly good with what FormLoginConfigurer registers as their out-of-the-box implementations: LoginUrlAuthenticationEntryPoint and AccessDeniedHandlerImpl. We customize the behavior of AccessDeniedHandlerImpl by setting the access denied page URL to redirect the users to our custom error page when the access is denied:

.exceptionHandling(customizer -> customizer.accessDeniedPage("/no-access"))

The default login page is generated by DefaultLoginPageGeneratingFilter (in this example project, we will be leveraging it).
But, if we would want to have our custom login page displayed instead of the default one, we need to invoke the “loginPage(…)” method on HttpSecurity DSL (any path, that we wish our login page at, can be passed as a parameter to the method, but we will stick to the “/login”) and create an endpoint in the controller, that will serve a custom login page at the specified URL (in our case, “GET /login”). It will result in DefaultLoginPageGeneratingFilter not being registered in the SecurityFilterChain.

http.formLogin(c -> c.loginPage("/login"))

Note, that public access to the provided URL will not be granted by default, but it can be changed by calling the “permitAll()” method on the configurer:

http.formLogin(c -> c.loginPage("/login").permitAll())

POST “/login” will remain to be implemented by the framework (that is, by the way, implemented by UsernamePasswordAuthenticationFilter, which simply forwards requests without any processing for all the requests except POST “/login”).
Note: In the “Form Login” setup, AuthenticationManager (subsequently, also AuthenticationProvider and UserDetailsService) is invoked only during the user login request. After the successful login [POST “/login”], AbstractAuthenticationProcessingFilter (which is a parent class of UsernamePasswordAuthenticationFilter) will reach out to SecurityContextRepository (with the implementation of HttpSessionSecurityContextRepository) to save the SecurityContext into the HttpSession. For the subsequent requests, the SecurityContext will be fetched from HttpSession (due to SecurityContextHolderFilter, which sets a Supplier for deferred loading of the SecurityContext from SecurityContextRepository when SecurityContextHolder “.getContext()” method is invoked).

Note, that CSRF protection is enabled in this example (as it should be for web-based applications that handle HTML forms). It means that submitting a POST form (in general, any non-GET requests) will require a specific parameter to be set, otherwise, requests will be denied by CsrfFilter. To set this parameter, all the POST forms should contain the following input:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

Form login with custom AuthenticationProvider

Source code: https://github.com/Igor-Polatajko/spring-boot-security-web/tree/form-login-authentication-provider

Press enter or click to view image in full size

AuthenticationProvider is designed to implement different authentication strategies based on the type of Authentication. As per the architecture of Spring Security, AuthenticationProvider(s) tend to be used with the dedicated implementation of AuthenticationManager, called ProviderManager, which implements the logic of choosing the “right” AuthenticationProvider implementation based on the type of the Authentication (it will receive an “unauthenticated” Authentication as a parameter). ProviderManager decides which AuthenticationProvider to invoke based on the return value of the “supports(…)” method of AuthenticationProvider.

Get Ihor Polataiko’s stories in your inbox

Join Medium for free to get updates from this writer.

Potentially, there can be a couple of AuthenticationProvider implementations, that return “true” from the “supports(…)” method for the particular type of Authentication: ProviderManager will keep invoking them one by one until some of them will return an object of Authentication (“authenticated”: with “isAuthenticated()” returning “true”) as the result (or throw an exception). Returning “null” from AuthenticationProvider’s “authenticate(…)” method indicates to the ProviderManager that particular AuthenticationProvider sort of “skips”, because of not being able to evaluate the authentication, so the next AuthenticationProvider (which supports the given type of the authentication) should be tried. If none will perform authentication, the exception will be raised.

If AuthenticationProvider implementation knows how the particular Authentication can be authenticated, it takes the responsibility of checking for a particular request, it should either return “authenticated” Authentication or throw an instance of AuthenticationException.

Usually, you would have only one AuthenticationProvider implementation per one implementation of the Authentication interface.

There are not many differences from the previous implementation with custom UserDetails service implementation, let’s see what is different:

AuthUser no longer needs to implement UserDetails, as the DaoAuthenticationProvider and UserService are no longer in use, and, UsernamePasswordAuthenticationToken implementation of Authentication (created by UsernamePasswordAuthenticationFilter upon login) has “Object” as a type of “principal”.

We have to make our custom implementation of AuthenticationProvider (instead of the UserDetailsService implementation like in the previous example). It should accept an instance of UsernamePasswordAuthenticationToken because this specific subtype of the Authentication will be created by UsernamePasswordAuthenticationFilter (which implements login functionality during the “form login” authentication flow and is registered because we call “.formLogin(…)” on HttpSecurity DSL). We can implement “.authenticate(…)” method in the way we want, but we have to remember the following:

  • If authentication should be failed (e.g., due to incorrect credentials, etc), AuthenticationProvider implementation should throw some instance of AuthenticationException
  • If authentication is successful, an instance of the same type of Authentication (in this case UsernamePasswordAuthenticationToken) should be returned, with “authenticated” set to “true”.

Implementation of loading the user data by username is pretty similar to our previous examples, but we have to now check ourselves if the password from the Authentication matches the password [hash] from the database.

package com.ihorpolataiko.springbootsecurityweb.security;


import com.ihorpolataiko.springbootsecurityweb.dto.user.UserResponseWithCredentials;
import com.ihorpolataiko.springbootsecurityweb.security.exception.ApplicationAuthenticationException;
import com.ihorpolataiko.springbootsecurityweb.security.user.AuthUser;
import com.ihorpolataiko.springbootsecurityweb.service.UserService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


@Component
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {


// We are free to implement any authentication logic we want.
// In our case, we use our existing UserService to load the user data by username, and then we
// will check if the password, provided in the request, matches the hash of the password from the
// database
private final UserService userService;


private final PasswordEncoder passwordEncoder;


public UsernamePasswordAuthenticationProvider(
UserService userService, PasswordEncoder passwordEncoder) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}


@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {


UserResponseWithCredentials userCredentialsByUsername =
userService.getUserCredentialsByUsername(authentication.getName());


if (!passwordEncoder.matches(
authentication.getCredentials().toString(), userCredentialsByUsername.passwordHash())) {
throw new ApplicationAuthenticationException("Bad credentials");
}


AuthUser authUser =
new AuthUser(
userCredentialsByUsername.userResponse().id(),
userCredentialsByUsername.userResponse().roles(),
userCredentialsByUsername.passwordHash());


// if null would be returned, then another implementation of authentication provider,
// that support given type of the authentication will be invoked
return new UsernamePasswordAuthenticationToken(
authUser, authentication.getCredentials(), authUser.getAuthorities());
}


@Override
public boolean supports(Class<?> authentication) {


// UsernamePasswordAuthenticationToken is the implementation of the Authentication,
// created by UsernamePasswordAuthenticationFilter which is registered
// in Spring Security when we configure .formLogin() via HttpSecurity DSL
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

The way we configure SecurityFilterChain via HttpSecurity DSL in the SecurityConfig class should be updated to register “.authenticationProvider(…)”:

package com.ihorpolataiko.springbootsecurityweb.config;


import com.ihorpolataiko.springbootsecurityweb.security.UsernamePasswordAuthenticationProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;


@EnableMethodSecurity // allow to specify access via annotations
@Configuration
public class SecurityConfig {


private final UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider;


public SecurityConfig(
UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider) {
this.usernamePasswordAuthenticationProvider = usernamePasswordAuthenticationProvider;
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


http.formLogin(Customizer.withDefaults())
// provide authentication provider implementation
// you can specify many authentication providers,
// ProviderManager will invoke them based on the type of
// the Authentication which was created earlier in the chain of security filters
// .authenticationProvider(...)
// .authenticationProvider(...)
// .authenticationProvider(...)
.authenticationProvider(usernamePasswordAuthenticationProvider)
// allow public access to the home page
.authorizeHttpRequests(mather -> mather.requestMatchers("/").permitAll())
// deny requests to API for this example
.authorizeHttpRequests(mather -> mather.requestMatchers("/api/*").denyAll())
// deny requests to Swagger UI
.authorizeHttpRequests(
mather ->
mather
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/*",
"/v3/api-docs",
"/v3/api-docs/swagger-config")
.denyAll())
.authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated())
.exceptionHandling(customizer -> customizer.accessDeniedPage("/no-access"));


return http.build();
}
}

Whenever you register AuthenticationProvider via HttpSecurity DSL, under the hoods ProviderManager implementation of AuthenticationManager will be instantiated and registered as AuthenticationManager.

Additionally, note that whatever piece of custom logic we register via HttpSecurity DSL to perform actual authentication logic (AuthenticationManager/AuthenticationProvider/UserDetailsService) will be used by the framework (by default) only in case we configure some out-of-the-box authentication mechanism (form login / http basic / etc), otherwise, the framework will just ignore them and leave them uninvoked. It happens, because whenever we register (AuthenticationManager/AuthenticationProvider/UserDetailsService via HttpSecurity methods, all the corresponding classes are registered internally in HttpSecurity (in the place called “sharedObjects”), so if we want to leverage them by our logic (e.g., we want the registered via HttpSecurity object of AuthenticationProvider to be invoked to authenticate the “unauthenticated” object of the Authentication, that we put into SecurityContext by our custom filter) we need to get the required objects from HttpSecurity’s “sharedObjects” manually (we will see how to do it in the examples with custom configurers).
If you got used to using
“authorizeRequests(…)” in the older versions of Spring Security (e.g., Spring 5.x.x / Spring Boot 2.x.x), you might remember that AuthenticationProvider(s) or AuthenticationManager, registered via HttpSecurity, were automatically picked up and executed to try to authenticate the Authentication object present in the SecurityContext: it was possible to just create “unauthenticated” Authentication by some filter registered in SecurityFilterChain, put it into the SecurityContext, and, registered via HttpSecurity, AuthenticationProvider(s) or AuthenticationManager, which supported the given type of Authentication, were triggered automatically. Well, that was due to FilterSecurityInterceptor filter, which was registered at the very end of the SecurityFilterChain by the ExpressionUrlAuthorizationConfigurer, which is applied when “authorizeRequests(…)” is invoked on HttpSecurity. This filter was attempting to authenticate the Authentication object (fetched from the SecurityContext) by using AuthenticationManager (registered via HttpSecurity), in case the Authentication object was not set to “authenticated” already, and then attempted to perform authorization. In Spring Security 6.x.x, “authorizeRequests(…)” and FilterSecurityInterceptor filter are marked as deprecated. Instead, we are using “authorizeHttpRequests(…)” method of HttpSecurity, which applies AuthorizeHttpRequestsConfigurer, that registers AuthorizationFilter to perform authorization of the request based on the rules defined via “.authorizeHttpRequests(…)” method of HttpSecurity (because method security rules are checked later on the Spring interceptors level) But, AuthorizationFilter does not attempt to authenticate the “unauthenticated” Authentication, it only performs authorization logic. That is the reason why if we want to leverage AuthenticationManager or AuthenticationProvider, registered via HttpSecurity, by our custom authentication mechanism implementations, we have to fetch it manually from HttpSecurity’s “sharedObjects” [e.g., BasicAuthenticationFilter and UsernamePasswordAuthenticationFilter are configured with AuthenticationManager from “sharedObjects”, which is injected into them during the creation via the corresponding configurers: HttpBasicConfigurer and FormLoginConfigurer]. By the way, “authorizeHttpRequests(…)” was also present in Spring Security, since version 5.6, but was less popular due to its relative novelty.
“authorizeRequests(…)” and “authorizeHttpRequests(…)” were never possible to use in the same HttpSecurity configuration].

Form login with custom AuthenticationManager

Source code: https://github.com/Igor-Polatajko/spring-boot-security-web/tree/form-login-authentication-manager

Press enter or click to view image in full size

Finally, we have come to the point where we override the most top-level component in the out-of-the-box Spring Security setup: AuthenticationManager. In the previous examples, we were leveraging built-in implementation: ProviderManager, which operates AuthenticationProvider implementations and chooses the right one to authenticate the request (by the way, ProviderManager also supports delegation to the parent AuthenticationManager if no AuthenticationProvider was able to take responsibility for the authentication of the request).

In this example, we will implement our own AuthenticationManager. API of this interface is pretty basic: only the “authenticate(…)” method to implement, which accepts an object of Authentication (“unauthenticated”) and returns also an object of Authentication (“authenticated”): the implementation details (how exactly you want authentication to happen) is up to you.

Let’s take a look at our custom implementation of the AuthenticationManager. In our case, the logic of authentication looks pretty much the same as in the previous example with custom AuthenticationProvider:

package com.ihorpolataiko.springbootsecurityweb.security;


import com.ihorpolataiko.springbootsecurityweb.dto.user.UserResponseWithCredentials;
import com.ihorpolataiko.springbootsecurityweb.security.exception.ApplicationAuthenticationException;
import com.ihorpolataiko.springbootsecurityweb.security.user.AuthUser;
import com.ihorpolataiko.springbootsecurityweb.service.UserService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;


@Component
public class CustomAuthenticationManager implements AuthenticationManager {


// We are free to implement any authentication logic we want.
// In our case, we use our existing UserService to load the user data by username, and then we
// will check if the password, provided in the request, matches the hash of the password from the
// database
private final UserService userService;


private final PasswordEncoder passwordEncoder;


public CustomAuthenticationManager(UserService userService, PasswordEncoder passwordEncoder) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}


@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {


UserResponseWithCredentials userCredentialsByUsername =
userService.getUserCredentialsByUsername(authentication.getName());


if (!passwordEncoder.matches(
authentication.getCredentials().toString(), userCredentialsByUsername.passwordHash())) {
throw new ApplicationAuthenticationException("Bad credentials");
}


AuthUser authUser =
new AuthUser(
userCredentialsByUsername.userResponse().id(),
userCredentialsByUsername.userResponse().roles(),
userCredentialsByUsername.passwordHash());


return new UsernamePasswordAuthenticationToken(
authUser, authentication.getCredentials(), authUser.getAuthorities());
}
}

Another thing we have to do to make everything work is to register our CustomeAuthenticationManager via HttpSecurity DSL (this way, during “httpSecurity.build()” it will be leveraged by the security filters, that rely on “sharedObjects” from HttpSecurity: like, for e.g., UsernamePasswordAutheneticationFilter):

package com.ihorpolataiko.springbootsecurityweb.config;


import com.ihorpolataiko.springbootsecurityweb.security.CustomAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;


@EnableMethodSecurity // allow to specify access via annotations
@Configuration
public class SecurityConfig {


private final CustomAuthenticationManager customAuthenticationManager;


public SecurityConfig(CustomAuthenticationManager customAuthenticationManager) {
this.customAuthenticationManager = customAuthenticationManager;
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


http.formLogin(Customizer.withDefaults())
// provide authentication manager implementation
.authenticationManager(customAuthenticationManager)
// allow public access to the home page
.authorizeHttpRequests(mather -> mather.requestMatchers("/").permitAll())
// deny requests to API for this example
.authorizeHttpRequests(mather -> mather.requestMatchers("/api/*").denyAll())
// deny requests to Swagger UI
.authorizeHttpRequests(
mather ->
mather
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/*",
"/v3/api-docs",
"/v3/api-docs/swagger-config")
.denyAll())
.authorizeHttpRequests(matcher -> matcher.anyRequest().authenticated())
.exceptionHandling(customizer -> customizer.accessDeniedPage("/no-access"));


return http.build();
}
}

--

--

Ihor Polataiko
Ihor Polataiko

Written by Ihor Polataiko

Passionate about System Architecture and Software Engineering. LinkedIn: https://linkedin.com/in/ihor-polataiko