Propagating the Spring SecurityContext to your Kotlin Coroutines
Spring Security provides a lot of convenience to develop secure web applications.
However, it relies strongly on a SecurityContext
stored in a thread-local (inside the SecurityContextHolder
class).
If not mitigated, this causes issues in multi-threaded contexts. When using Kotlin Coroutines, there is an additional
abstraction layer where you don’t really know (and don’t want to know) on which thread(s) your code will be running.
Luckily, there is a relatively easy solution!
The default Spring Security approach of storing the security context in a thread-local works well in traditional servlet applications where a request is fully handled on one specific thread. Spring also provides additional support when using an Async servlet, or when you are creating your own threads or executors. If you are using Spring WebFlux (reactive Spring), Kotlin Coroutines already work in combination with EnableReactiveMethodSecurity. In other cases, when using Coroutines in a 'traditional' Spring MVC environment, another approach is required.
Kotlin Coroutines are not bound to a specific thread, and therefore do not work well with thread-local variables by default. To support thread-locals inside your coroutines, the Coroutines library provides a class called ThreadContextElement. This class allows you to create a coroutine context which will enable you to populate the context of a thread (thread-local variables) when your coroutine gets resumed on that specific thread.
The implementation
We will create an implementation of a ThreadContextElement
which will propagate the Spring SecurityContext
to the
threads running your Coroutines. The complete source code for the class is first shown below, followed by the explanation
of the different parts, and instructions on how to use it.
class SecurityCoroutineContext(
private val securityContext: SecurityContext = SecurityContextHolder.getContext() (1)
) : ThreadContextElement<SecurityContext?> {
companion object Key : CoroutineContext.Key<SecurityCoroutineContext>
override val key: CoroutineContext.Key<SecurityCoroutineContext> get() = Key (2)
override fun updateThreadContext(context: CoroutineContext): SecurityContext? { (3)
val previousSecurityContext = SecurityContextHolder.getContext()
SecurityContextHolder.setContext(securityContext)
return previousSecurityContext.takeIf { it.authentication != null }
}
override fun restoreThreadContext(context: CoroutineContext, oldState: SecurityContext?) { (4)
if (oldState == null) {
SecurityContextHolder.clearContext()
} else {
SecurityContextHolder.setContext(oldState)
}
}
}
1 | The constructor accepts an instance of the SecurityContext , which by default gets populated with the security context
from the current thread (which is what we want). It is defined as a constructor parameter for testing reasons,
to allow passing another security context through the constructor. |
2 | To store elements inside a coroutine context, an instance of CoroutineContext.Key is required. Basically, the
coroutine context can be seen as a Map<CoroutineContext.Key<T>, T?> . When defining a Coroutine context element,
a singleton Key is required to be able to reference it. We define that Key as a companion object. |
3 | The updateThreadContext function is automatically called when a coroutine gets started or resumed on a certain thread.
The function determines which SecurityContext was previously stored on the thread, and sets the security context
to the one we received in the constructor. The function returns the previously stored value, so it can be restored later.
Because SecurityContextHolder.getContext() never returns a null object, but creates a new empty context when no context exists yet,
we only keep track of the previous context when it actually contains an authentication object (We don’t want to litter the thread-locals with empty security contexts). |
4 | The restoreThreadContext function is automatically called when the coroutine gets suspended, and restores the previous context.
The oldState variable is the object that was returned from the updateThreadContext function earlier, so if the thread
previously had no security context, the context is cleared, and if another security context was present, that context is restored. |
How to use it?
To use the SecurityCoroutineContext
, simply add it as a context element to your coroutine scopes. For example:
val coroutineScope = CoroutineScope(Dispatchers.IO)
// ...
coroutineScope.launch(SecurityCoroutineContext()) {
// The SecurityContextHolder is populated here (and in every child coroutine) with the SecurityContext from the thread that called the `launch` function.
}
Or when using the GlobalScope
:
GlobalScope.launch(SecurityCoroutineContext()) {
// Do async stuff here within the correct security context
}
Of course, it’s also possible to combine this Coroutine context element with other elements, e.g.:
runBlocking(Dispatchers.Default + SecurityCoroutineContext()) {
// Do async stuff here within the correct security context
}
Compiled and tested with Kotlin version 1.5.21 and Spring Boot versions from 2.3.x to 2.5.x.