Over the years, I’ve had to implement security filters a couple of time. Recently I had to add JWT token based API authentication to a Spring project.
Some complicating factors:
- It’s the reactive variant of Spring, aka. Flux.
- Flux has a very complicated API surface.
- To not have to deal with that we use Kotlin & Co-routines. This too has a few rough edges still as it is very new.
- Servlet Filters don’t work when using Flux because they are inherently synchronous. So we have to do things the Flux way.
So, I spent a good amount of time to figure out how to do this correctly and this is another instance of me documenting by blogging so I don’t have to google my way through the maze of cryptic documentation again. Also, I hope others might find this useful.
High-level design
The design for my solution is fairly simple. I never really liked Spring Security as it mainly gives me headaches. Also I have custom requirements now and more coming that just won’t fit in what it does that easily (I speak from experience). But I imagine it does something similar.
So instead:
- We use simple JWT tokens that are signed, have a payload with things like a userId, and need to be checked as part of our Authentication and Authorization logic. Standard stuff these days.
- Any request that includes an Authorization header,
we want to grab the JWT token from there, validate it,
and create a
SecurityContext
object. This object forms the basis for our authorization logic. - The Authorization logic lives in an
AuthorizationService
that is called to run checks from our business logic. When that happens, it needs to grab theSecurityContext
check whether we authenticated, grab the userId, and figure out the set of roles and privileges (beyond the scope of this article) the principal has.
So, in short, we need something that creates the
security context and stuffs it in a place where the
AuthorizationService can grab it. Since we use
co-routines on top of Flux, that place is the Flux
Reactor Context and we want to get to that via the
coroutineContext
that is part of the
co-routine scope all our logic executes in.
The filter
Spring Flux offers two ways to implement something
similar to the good old ServletFilter
,
which is what you’d use when we were all still doing
synchronous IO with Tomcat. One of these is called
WebFilter
, this appears to be the most
useful of the two, since crucially it returns something
called a ServerWebExchange
, which in a
somewhat convoluted way gives us access to the request
Flux
and the ability to interact with the
Spring Reactor Context
. The best way to
think of that is as a ThreadLocal
like
construct for Flux where we can park custom data and
access it downstream. Via the
coroutines-reactor
library, we gain a few
feature to access this via the co-routine scope.
The other way to filter is via
HandlerFilterFunction
which looks like it’s
a bit more limited as it does not provide an obvious way
to do anything with Flux (correct me if I’m wrong) but
would be a better fit if you use the Spring’s router
DSL.
@Component
class AuthorizationWebFilter(val tokenService: TokenService): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val jwtToken = tokenService.parseHeader(
.request.headers["Authorization"]?.firstOrNull())
exchangeval context = tokenService.createSecurityContext(jwtToken)
return chain
.filter(exchange)
.subscriberContext {
.put(FormationSecurityContext::class.java, context)
it
}
} }
This is where Spring’s API gets a little weird. This
reads different then what it actually does and this
threw me off for an while. The key here is that you call
subscriberContext
on the return value of
.filter(exchange)
. To me this reads like:
first do the request logic and then mess with the
context. Luckily, what it does is different and the
context gets modified before the logic kicks in. Just a
bit of API weirdness.
The put method is weirder. Especially in combination
with how we are getting values from the reactor Context.
Intellij suggests a type of Any
for both
key and value. This is a lie and just where the Java
type system fell a bit short, I guess. The correct types
for this are Class<T>
and
T
. So, it’s a map indexed by the class of
the value. In our case that would be
FormationSecurityContext
.
A final gotcha is that unlike most Map
implementations, put does not manipulate a Context but
creates a new one. I initially had this because I
assumed put did not have a return value.
{
subscriberContext // this is wrong!
.put(FormationSecurityContext::class.java, context)
it
it }
So, that looks like a deceptively easy bit of code but it was made hard by a lack of documentation, and Spring not following the principle of the least amount of surprise, which makes all this hard to discover.
Getting the value out on the other side
Now that we have our security context, we want to use it. For this I implemented a simple DSL to check auth in places where we need that. This is the Kotlin way and I prefer it over annotations and/or AOP based madness.
// ReactorContext is still experimental
@ExperimentalCoroutinesApi
@Component
class AuthorizationService(val roleRepository: RoleRepository) {
/**
* Runs the block if the authorization checks succeeed or throws a `NotAuthorizedException`.
*/
fun <T> authorize(privilege: Privilege,ownerId: String, block: suspend ()->T):T {
suspend val reactorContext = coroutineContext[ReactorContext]
val securityContext = reactorContext?.context?
.get(FormationSecurityContext::class.java)
(securityContext == null) {
if// should not happen; means our AuthorizationWebFilter is broken
throw IllegalStateException("no context")
} else {
if(!securityContext.isAuthenticated)
throw NotAuthorizedException(AuthProblemCode.JWT_MISSING)
// additional auth checks beyond the scope of this article
(privilege,ownerId,securityContext.user
checkAuth?: throw IllegalStateException("no user"))
return block.invoke()
}
}
}
To use this, you simply do something like this:
fun getUserProfile(userId: String): UserProfile =
suspend .authorize(UserProfilePrivilege.GET_USER_PROFILE, userId) {
authorizationService.findUser(userId)?.toUserProfile() ?: throw NotFoundException(userId)
userRepository}
It’s a suspend function because we are using this from Flux based flow. In this case, we are actually using the Expedia GraphQL integration for Spring, which is definitely beyond the scope of this article but quite easy to set up.
But if you weren’t, you could do something like this to create an endpoint:
{
coRouter ("/user/{userId}") {
GET().contentType(MediaType.APPLICATION_JSON)
ok.bodyValueAndAwait(userProfileSerice.getUserProfile( it.pathVariable("userId")))
}
}
The bodyValueAndAwait
extension function
takes our suspending function and turns it into a Spring
Mono
, so Spring Reactor does the right
things with Flux.