JSON Web Token Verification in Ktor using Kotlin and Java-JWT

Kotlin

Ktor

In my previous article, we looked at how to get an access token and use it to access a protected resource, in Kotlin. Now we’re going to take a look at the other side of the story: how to validate an access token (in this case a structured JWT) before allowing access to the protected resource.

For token verification we’re going to:

  1. Get available public keys from a JWKS endpoint
  2. Parse the public key used to sign the receive JWT
  3. Verify the access token signature, issuer, and audience. This will also verify that the token hasn’t expired (the exp claim), that it was issued in the past (the iat claim), and that the token is allowed to be used (the nbf claim)

We’ll then use this logic to protect an API endpoint running on Ktor.

Choosing a JWT Validation Library

You really don’t want to roll your own JWT verification library, and whilst it would be nice to have a Kotlin specific JWT library, there are already 4 pretty decent implementations written in Java that work seamlessly from Kotlin.

For finding a JWT library, the first stop is jwt.io. This site includes a breakdown of features supported by each library, what JWT values it will check, and what signature algorithms it supports.

I had a quick play around with each of the libraries listed on jwt.io, and ended up using the Java JWT library from Auth0 (the maintainers of jwt.io) as I preferred their API, and they also have a library available for JWKs handling, which we also want to take advantage of. Brian Campbell’s jose4j was a close second, with a similar feature set but I wasn’t as keen on its API.

Getting a Public Key from a JWKS Endpoint

We’re going to be getting the public key to verify our JWT from a JSON Web Key Set endpoint. This endpoint would be provided by your authorization server to enable client applications to programmatically discover JSON Web Keys, and to allow the authorization server to publish new keys without having to notify and distribute them manually to all client application owners. This allows for painless key rollover and integration.

So, first let’s bring in jwks-rsa, the package we’re going to use for fetching and parsing JWKs. For examples in this article, I’ll be using Gradle:

compile "com.auth0:jwks-rsa:0.3.0"

Now we can fetch the our JWKS using:

val jwkProvider = UrlJwkProvider(
    URL("http://localhost:5000/.well-known/openid-configuration/jwks"))

In this case I am specifying the URL myself. There is an overload on this method that accepts a string, but this overload then appends /.well-known/jwks.json, which is the OAuth version of OpenID Connect’s discovery document (which is still in beta, but from a quick google seems to be already in use by Auth0 and AWS Cognito).

Now we need to see which key in the set was used to sign our token. So first we’ll need to retrieve the key ID from the token we received. We could do this ourselves, by base64 decoding the header of the JWT, parsing the JSON and retrieving the kid, but the JWT library we’ll be using soon has a handy decode method that we can use. So, let’s import the java-jwt library now:

compile "com.auth0:java-jwt:3.3.0"

And then decode our token using:

val jwt = JWT.decode(token)

Now we can use our JWK provider to find the public key required to validate our JWT, using the kid from the tokens header:

val jwk = jwkProvider.get(jwt.keyId)

If a key with that identifier is not found, this will throw a SigningKeyNotFoundException.

Verifying a JWT

Picking an Algorithm

Now that we have a JWK, we can verify our JWT. First thing we need to do is to figure out what algorithm was used to sign the token. Since we’ve already decoded the token, we can again use a value in the tokens header, this time the alg parameter.

For now, we’re only going to handle RSA256, which is the alg value of RS256. You can find a full list of supported algorithms on here.

val algorithm = when (jwk.algorithm) {
    "RS256" -> Algorithm.RSA256(/*public key*/, /*private key*/)
    else -> throw Exception("Unsupported Algorithm")
}

Since we are verifying a signature, we only need to supply the public key, however the jwk.publicKey property currently returns java.security.PublicKey, whilst the JWT library is expecting a java.security.RSAPublicKey, so we’re going to need to do a bit of casting:

val publicKey: RSAPublicKey = jwk.publicKey as RSAPublicKey

Or a bit more safely with:

val publicKey = jwk.publicKey as? RSAPublicKey ?: throw Exception("Invalid key type")

We can now pass the public key to our algorithm picker, along with a null private key.

Token Verifier

To configure how we want to verify the token, the library uses the builder pattern:

val verifier = JWT.require(algorithm) // signature
        .withIssuer("http://localhost:5000") // iss
        .withAudience("api1") // aud
        .build()

Here we are configuring the following verification steps:

  1. Verify the token signature using the defined algorithm, ensuring the data in the token has not been tampered with
  2. Verify that the token was issued by the expected authority
  3. Verify that the token was issued to be used with this protected resource (audience)
  4. Verify that the token was issued in the past, has not expired, and is allowed be used yet (the iss, exp & nbf claims)

There are also methods for verifying the JWT ID and Subject, but we're not going to be using these.

And finally, with our built verifier, we can verify the token and return the resulting DecodedJWT:

return verifier.verify(token)

If any of our checks fail, an exception will be thrown by the Java-JWT library.

Ktor API

Currently all we’ve really done is write some Java with a slightly different syntax, so let’s make things Kotlin specific by protecting a Ktor API with our JWT validation.

First, we need to setup Ktor and create an API endpoint. To set up Ktor, follow one of the quickstart guides on ktor.io. We’ll create an API endpoint that simply returns some text, leaving you with an app that looks something like the following:

fun main(args: Array<String>) {
    val server = embeddedServer(Netty, 8080) {
        routing {
            get("/") {
                call.respondText("Ktor Working!", ContentType.Text.Html)
            }
            get("/api") {
                call.respondText("Hello from protected resource!")
            }
        }
    }

    server.start()
}

We’re going to use some functionality found in the Ktor auth library, so let’s now also bring that in as a dependency:

compile "io.ktor:ktor-auth:0.9.0"

The first validation check we can do, is to look for the presence of an Authorization header and the Ktor auth library we just brought in has a handy function, just for that:

val authHeader = call.request.parseAuthorizationHeader()

If the header is not null, we can safely check the scheme and verify its value:

if (!(authHeader == null 
        || authHeader !is HttpAuthHeader.Single 
        || authHeader.authScheme != "Bearer")) {
    try {
        val jwt = verifyToken(authHeader.blob)
    } catch (e: Exception) {
        // ignore invalid token
    }
}

If our token is valid, we can safely set the current principal to track the caller. So let’s do that very simply, by creating a UserIdPrincipal that uses either the tokens sub claim (if the call is on behalf of a user) or client_id claim (if the call is on behalf of an application).

context.authentication.principal = 
    UserIdPrincipal(jwt.subject ?: jwt.getClaim("client_id").asString())

And now finally, we can return a 200 OK , or a 401 Unauthorized depending on if the principal was set:

if (call.principal<UserIdPrincipal>() != null) {
    call.respondText("Hello ${call.principal<UserIdPrincipal>()?.name}!")
} else {
    call.respond(UnauthorizedResponse())
}

We can be a little friendlier and issue a HTTP “WWW-Authenticate” response header field, informing the requester what they need to do in order to access this resource.

call.respond(UnauthorizedResponse(
    HttpAuthHeader.Parameterized("Bearer", mapOf("realm" to "api1"))))

Ktor Authentication

We can make our authentication code a bit more reusable by making it a function on AuthenticationPipeline:

val JWTAuthKey: String = "JwtAuth"
fun AuthenticationPipeline.bearerAuthentication(realm: String) {
    intercept(AuthenticationPipeline.RequestAuthentication) { context ->
        // parse token
        val authHeader = call.request.parseAuthorizationHeader()
        val jwt: DecodedJWT? =
                if (authHeader?.authScheme == "Bearer" && authHeader is HttpAuthHeader.Single) {
                    try {
                        verifyToken(authHeader.blob)
                    } catch (e: Exception) {
                        null
                    }
                } else null

        // transform token to principal
        val principal = jwt?.let { 
            UserIdPrincipal(jwt.subject ?: jwt.getClaim("client_id").asString()) 
        }

        // set principal if success
        if (principal != null) {
            context.principal(principal)
        } else {
            // otherwise, challenge with WWW-Authenticate header & realm
            context.challenge(JWTAuthKey, NotAuthenticatedCause.InvalidCredentials) {
                it.success()
                call.respond(UnauthorizedResponse(HttpAuthHeader.bearerAuthChallenge(realm)))
            }
        }
    }
}

private fun HttpAuthHeader.Companion.bearerAuthChallenge(realm: String): HttpAuthHeader = 
    HttpAuthHeader.Parameterized("Bearer", mapOf(HttpAuthHeader.Parameters.Realm to realm))

We can then update our API endpoint to look like:

route("/api", HttpMethod.Get) {
    authentication {
        bearerAuthentication("api1")
    }
    handle {
        call.respondText("Hello ${call.principal<UserIdPrincipal>()?.name}")
    }
}

Source Code

You can find the full source code for this article on GitHub, including a demo authorization server created using IdentityServer 4 and .NET Core.

Keep Up To Date

Follow me on Twitter to keep up to date with the latest articles and announcements.