Skip to main content

Testing JWTs with ktor and JUnit Jupiter

ยท 5 min read

So, as we know, testing is important. Sure, manually testing your application before you ship it into production is good. But what about automated tests. What do we do when you need something which is not available from your framework? We built it ourselves.

The Problem

So I use ktor and JUnit Jupiter in my application. I wrote some code which handles authorization based on the permissions claim in JWTs. It's based on the regular JWT authentication support found in ktor. And because when authentication and authorization would not work correctly, I would have a very big problem, I decided to write automated tests. My framework, ktor, comes with great support for testing out of the box. But not for easy JWT authentication in tests. So I thought about it and came up with the idea to just create a JWT authentication configuration in my test and give it a verifier with a fixed secret. When I needed a valid token, I could just create one with the fixed secret.

First Solution

So, as I said, my first solution then looked like this.

@Test
fun `Request with a valid token succeeds`() = testApplication {
val algorithm = Algorithm.HMAC256("test secret")
val verifier = JWT.require(algorithm).build()
install(Authentication) {
jwt {
validate { JWTPrincipal(it.payload) }
verifier(verifier)
}
}
routing {
authenticate {
handle {
call.respond(HttpStatusCode.OK)
}
}
}
val status = client.get("/") {
bearerAuth(JWT.create().sign(algorithm))
}.status
assertThat(status).isEqualTo(HttpStatusCode.OK)
}

I created a new verifier, authentication configuration and a new token in the request. So that works fine, but could be improved. Imagine we're writing some more tests, and we would have to write the same JWT configuration code over and over again. We could just move our verifier and token into the class, but what if we want to test JWTs in multiple classes?

JUnit Extensions to the rescue

So JUnit 5 provides an API to extend it with new functionality. The description of JUnit Extensions in the JUnit 5 user guide is excellent. What we plan to do is that we can get the verifier and a valid token via function parameters. So that seems to be ell. There is even an example similar to what we want to do on GitHub.

So the first thing we do is creating a new class which extends ParameterResolver.

class JWTExtension : ParameterResolver

Now we have to implement 2 functions. supportsParameter and resolveParameter.

So let's start with supportsParameter. This function gets called by JUnit for every parameter we have in our tests. And we just tell JUnit if we can provide a value for that parameter. So we want to provide values for JWTVerifier and String parameters.

override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext,
): Boolean {
return parameterContext.parameter.type == JWTVerifier::class.java ||
parameterContext.parameter.type == String::class.java
}

But how can we differentiate between other String parameters and the ones where we want to provide a token? We can create an annotation for that.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class TestJWT

So let's add that to our supportsParameter function.

override fun supportsParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext,
): Boolean {
return parameterContext.parameter.type == JWTVerifier::class.java ||
parameterContext.parameter.type == String::class.java &&
parameterContext.isAnnotated(TestJWT::class.java)
}

Now let's continue and implement resolveParameter. We also need to add a field to the class.

class JWTExtension : ParameterResolver {
private val algorithm = Algorithm.HMAC256("test secret")
// ...
override fun resolveParameter(
parameterContext: ParameterContext,
extensionContext: ExtensionContext,
): Any {
return when (parameterContext.parameter.type.kotlin) {
JWTVerifier::class -> {
JWT.require(algorithm).build()
}
String::class -> {
JWT.create().sign(algorithm)
}
else -> error("Invalid type.")
}
}
}

So let's adjust our test.

@Test
fun `Request with a valid token succeeds`(verifier: JWTVerifier, @TestJWT token: String) =
testApplication {
install(Authentication) {
jwt {
validate { JWTPrincipal(it.payload) }
verifier(verifier)
}
}
routing {
authenticate {
handle {
call.respond(HttpStatusCode.OK)
}
}
}
val status = client.get("/") {
bearerAuth(token)
}.status
assertThat(status).isEqualTo(HttpStatusCode.OK)
}

Now let's enable JUnit's automatic extension loading mechanism in our build.gradle.kts.

tasks {
withType<Test> {
useJUnitPlatform()
systemProperty("junit.jupiter.extensions.autodetection.enabled", true)
}
}

Don't repeat yourself

So, but what is when we are writing multiple tests. We would still have to repeat some code. And that's bad. Let's fix that. Let's write a little utility function where we provide sense defaults for our tests.

internal fun testApplicationWithJWT(
verifier: JWTVerifier,
token: String?,
routing: Routing.() -> Unit = {
authenticate {
handle {
call.respond(HttpStatusCode.OK)
}
}
},
expectedCode: HttpStatusCode = HttpStatusCode.OK,
request: HttpRequestBuilder.() -> Unit = {
token?.let(::bearerAuth)
},
block: suspend ApplicationTestBuilder.(HttpResponse) -> Unit = {},
) = testApplication {
install(Authentication) {
jwt {
validate { JWTPrincipal(it.payload) }
verifier(verifier)
}
}
routing(routing)
val response = client.get("/", request)
assertThat(response.status).isEqualTo(expectedCode)
block(response)
}

So let's adjust our test again.

@Test
fun `Request with valid token succeeds`(
verifier: JWTVerifier,
@TestJWT("test") token: String,
) = testApplicationWithJWT(verifier = verifier, token = token)

And let's write another one.

@Test
fun `Request without token fails with Unauthorized`(verifier: JWTVerifier) =
testApplicationWithJWT(
verifier = verifier,
token = null,
expectedCode = HttpStatusCode.Unauthorized
)

So of course in a real world application, we would write some more tests, but I'll leave that to you :)

View Full Example

Used Libraries: