Well, at least if you want to run multiple instances of your application. And, additionally, it’s not necessary, at least in most cases. There’s another mechanism for keeping some user session state in a web application. It’s called Cookie. And, instead of using it only to store a session identifier, why not let it hold the data itself. This blog post shows, that, with some effort, it’s possible to configure Spring Security to store its session information in a cookie instead of a server-side session.
Spring Security architecture
Spring Security integrates into Spring web as a servlet request filter
(see Chapter 9 of the Spring Security Reference).
FilterChainProxy is the central filter class and contains a parallel
(see Chapter 9.4 of the Spring Security Reference).
FilterChainProxy is also a good starting point for debugging the Spring Security processing.
In our sample project (using Spring Boot 2.3.1 and Spring Security 5.3.3) the
SecurityFilterChain contains the
following filters (identified by debugging into
FilterChainProxy.doFilter(...) and looking into
Let’s have a closer look at those filters that are relevant for our purpose and how to extend and customize their behaviour.
From the API documentation: “Populates the
SecurityContextHolder with information obtained from the configured
SecurityContextRepository prior to the request and stores it back in the repository once the request has completed
and clearing the context holder.”
SecurityContext mainly represents the persisted session. It contains an
Authentication which in the context of
a web application encapsulates the information of the authenticated user. The default implementation of the
SecurityContextRepository stores the
SecurityContext in the
HttpSession. To change this behaviour we have
to provide our own
UserInfo in our sample project is a very simple POJO that implements the
UserDetails interface and contains the
information that we want to hold in our user session.
SaveToCookieResponseWrapper gets the
UserInfo from the
SecurityContext and puts it into a
SignedUserInfoCookie is an extension of
javax.servlet.http.Cookie that handles the serialization and deserialization
UserInfo into/from the cookie value.
The cookie value has to follow RFC-6265 which allows only a few non-alphabetical characters (see Stack Overflow answer for a good summary), for example no whitespace, quotes or brackets are allowed. So we can’t use a JSON structure to serialize our payload, which would probably be easier to handle, especially to parse. We could have encoded the payload with Base64 before writing it into the cookie. However, the idea of the sample project was to keep the cookie value unencoded and human-readable, so we decided for the individual format.
As the cookie contains the id and the roles of the authenticated user, we have to make sure that the value is not modified on the client side. To do this our sample application signs the cookie by computing a HMAC (hash-based message authentication code) of the payload and appending it to the cookie value. That’s a quite simple approach and there are probably better and more secure ways of securing the cookie. One option might be JWT which provides a standardized way to securely exchange sensitive data. But, this is a topic of its own and out of the scope of this blog post.
(Thanks to Christian Köberl, @derkoe, for his feedback and ideas to improve the security of the cookie)
SecurityContext is requested via
HttpServletRequest is transformed into a
SignedUserInfoCookie again. The cookie value is verified using the HMAC signature.
CookieVerificationFailedException will be thrown if the received cookie is unsigned or the HMAC does not fit to the value.
UserInfo is retrieved from the
SignedUserInfoCookie, wrapped in a
UsernamePasswordAuthenticationToken and set into the
From the API documentation: “Processes an authentication form submission.”
See also Chapter 10 of the Spring Security Reference for a detailed description of the Spring Security authentication process.
UsernamePasswordAuthenticationFilter triggers the authentication, if necessary and possible. It reads username
and password from a login form request, wraps them into a
UsernamePasswordAuthenticationToken and calls the configured
AuthenticationManager to perform the authentication.
In the default configuration, the
AuthenticationManager is a
ProviderManager which holds a list of
AuthenticationProviders to which it delegates the authentication request. In our sample project we use a very basic
InMemoryAuthenticationProvider which knows only one static user. In a real world project we would instead use a database
or LDAP provider (from the Spring Security LDAP module).
After a successful login the configured
AuthenticationSuccessHandler is called. Usually, this handler decides about
where to forward the user to after the successful login. In the default configuration a
SavedRequestAwareAuthenticationSuccessHandler is used. It loads and replays the original request (which was cached before
ExceptionTranslationFilter, see next section) to show the page to the user which he/she originally
requested. As this
RequestCache is also stored in the server-side session, we have to find another strategy for this feature as well.
RedirectToOriginalUrlAuthenticationSuccessHandler extends the
SimpleUrlAuthenticationSuccessHandler and sets the
targetUrlParameter in its constructor. The parameter is defined and used by the extended
AbstractAuthenticationTargetUrlRequestHandler to find the target URL in the request parameters. Using this
feature, we can simply put the originally requested URL into a hidden input field of the login form.
determineTargetUrl(...) method of the
AbstractAuthenticationTargetUrlRequestHandler is overridden to prevent
tampering of the target URL parameter (see OWASP Unvalidated Redirects and Forwards Cheat Sheet).
We only expect relative URLs within our own application.
RedirectToOriginalUrlAuthenticationSuccessHandler also overrides the
onAuthenticationSuccess(...) method. In this method
we can get additional parameters (in our example a favorite colour) from the login form and add it to the
From the API documentation: “Handles any
thrown within the filter chain.”
Especially the very first, unauthorized request of a user triggers an
AccessDeniedException (somewhere out of the
FilterSecurityInterceptor). This one is catched and handled by the
If the user is not yet authenticated, the filter forwards him/her to the configured
In the default configuration, the original request is temporarily stored in a
RequestCache to be replayed after a
successful login (see previous section). As noted before, the default
HttpSessionRequestCache also uses the server session
to store the request. We could have introduced a
CookieRequestCache to stored the request in another cookie (like the
SecurityContext). In our sample project we follow another approach.
We deactivate the
RequestCache and instead extend the default
LoginUrlAuthenticationEntryPoint, which forwards the user
to the login form.
determineUrlToUseForThisRequest(...) method appends the URL from the original request as a query
parameter to the redirect URL. This way, the URL can be mapped to a hidden input field and will be included again in the
login request where the
AuthenticationSuccessHandler can read it (see previous section).
WebSecurityConfig configures Spring Security to use all the components described above.
To prevent the creation of the server-side session and the
JSESSION cookie we use the
To really activate this policy, we have to disable CSRF protection as well (see Spring Security issue 5299).
We use the
CookieSecurityContextRepository and our cookie should be deleted after the user logs out.
RequestCache is deactivated and instead the
LoginWithTargetUrlAuthenticationEntryPoint is used to add
the originally requested URL to the login form request.
RedirectToOriginalUrlAuthenticationSuccessHandler is used to forward the user to the originally requested URL after
a successful login.
Spring Security might seem to be very complex and hard to configure on the first glance. But, spending some time with the documentation and doing a little bit of debugging in a sample application, it turns out that it is very extensible and customizable by design. The hardest part is to find the right places. This blog post identified the ones that need to be adjusted to store the user session information in a cookie instead of a server-side session. It might also be a starting point for other, related topics.
Many thanks to Michael Vitz for showing me the relevant starting points and reviewing the resulting implementation. Also many thanks to Jochen Christ, Jan Stępień, and Stefan Tilkov for their feedback to this post.
Header Photo by Steve Halama on Unsplash