This Week I Learned: Go multi-node with Spring Security by externalising sessions to the database [2023–08–10]
So you’ve followed SSO with Spring Security in Kotlin and your service nicely redirects users to your SSO provider when needed, giving you details of who they are. You can debug locally and deploy. Nice. 🎉
However, when you increase your number of compute nodes to 2+ then users often get stuck in a redirect loop: service → sso → service → sso…
For me, this happened when I first deployed to an environment where the number of Kubernetes pods is 2 by default (locally and in the pre-prod environment it was 1).
What is happening?
Spring Security records a session for each browser user, and if it doesn’t have a session then it sends them to the SSO provider to log in. By default it stores this session in-memory in the compute node.
This session has an id, which Spring puts in the data sent to the SSO provider and back to one of your compute nodes, so when you have 2 nodes then there’s a 1/2 chance that after login it will get back a session that it didn’t create. Spring will reject this and send the browser user back to SSO, after which it has another 1/2 chance to happen again. If you have 3 nodes then the chance is 2/3 each time, if you have 4 then it is 3/4 etc.
What do you do?
You need to externalise Spring’s session storage. It needs to live outside of your compute nodes and be read by Spring on incoming requests.
Then when Spring looks at an incoming request that references a session that was started on a different node it will find the session in storage and show a successful logged-in response to your user.
What are your options?
Spring supports a few, and those most easily accessible are probably JDBC and Redis. I chose JDBC because I already had a database but not a Redis cluster, and this is a small app in which I’m not worried about overwhelming my database. If I were to have a Redis cluster already, or database performance concerns, then I may have chosen differently.
- Complete the steps in OAuth2 SSO with Spring Security in Kotlin.
- Add to your
application.ymlin your default profile:
initialize-schema: always # optional, but it worked for me
timeout: 604800 # 1 week; optional
3. Add to your
application.yml in your test profile, if you want to avoid every test trying to ping a real database (and probably failing):
store-type: # deliberately empty; use default in-memory storage
4. Add to your
build.gradle.kts to automagically read your config:
5. Remember to increase your number of nodes/pods to 2+ if you had previously set it to 1, so that you can scale up. 😎
The changes were pretty small in the end, but finding them in the Spring docs and various online articles was not.