Securing a Kotlin API with JWTs

Securing a Kotlin API with JWTs

Code Examples / api-integration

Overview

This tutorial explains how to integrate the jose4j security library and use it to validate JWTs in a plain Kotlin API. We will also use Spark Java as a minimal REST API stack.

Create a Kotlin API

Create your own Kotlin API according to a Spark Tutorial of your choice. Our API will have a simple entry point function which ensures that a valid access token has been provided before any API routes are allowed to run:

fun main() {

    val configuration = Configuration()
    port(configuration.getPortNumber())

    val filter = OAuthFilter(configuration)
    before("*") { request, response ->
        filter.doFilter(request.raw(), response.raw(), null)
    }

    val routes = ApiRoutes()
    get("/api/data", routes::getData)
}

Configuration

The code sample uses the Java configuration system and the following properties. The jose4j library will download token signing public keys from the Authorization Server’s JWKS endpoint, then use them to verify the signature of received JWTs. The library will also reject any access tokens whose issuer and audience claims do not match the API’s configured values:

port=3000
jwks_endpoint=https://idsvr.example.com/oauth/v2/oauth-anonymous/jwks
issuer_claim=https://idsvr.example.com/oauth/v2/oauth-anonymous
audience_claim=api.example.com

Security Library Integration

Next get the latest version of the jose4j library, which will perform the security work for us. When the OAuth filter is created it constructs global objects to manage downloading JWKS keys:

private val _httpsJkws = HttpsJwks(_configuration.getJwksEndpoint())
private val _httpsJwksKeyResolver = HttpsJwksVerificationKeyResolver(_httpsJkws)

Note that these classes also manage caching of JWKS keys in a thread safe manner. Any subsequent requests to the API with JWTs having the same kid value, do not trigger additional HTTPS calls to the JWKS endpoint, so that API behavior is efficient.

Next the token validation behavior is configured, and note that allowed algorithms are restricted to those expected from clients, as recommended in JWT Security Best Practices.

val jwtConsumer = JwtConsumerBuilder()
    .setVerificationKeyResolver(_httpsJwksKeyResolver)
    .setJwsAlgorithmConstraints(
        AlgorithmConstraints.ConstraintType.PERMIT,
        AlgorithmIdentifiers.RSA_USING_SHA256
    )
    .setExpectedIssuer(_configuration.getIssuerExpectedClaim())
    .setExpectedAudience(_configuration.getAudienceExpectedClaim())
    .build()

Finally the OAuth filter calls the jose4j processToClaims method, and for valid tokens this results in a claims object being returned, which is then attached to the request object so that it can be used in API routes:

val jwtClaims = jwtConsumer.processToClaims(jwt)
request.setAttribute("principal", jwtClaims)

Using Claims and Scopes

Once the token is validated, the API can access its scopes and claims via the Java request object. A real world API would then use the scopes and claims from the JWT to authorize access to business data, according to the below articles:

Our sample API simply returns scopes and claims back to the client, and in this example the JWT contains a custom claim called role:

class ApiRoutes {

    fun getData(request: Request, response: Response): String {

        val claims = request.attribute<JwtClaims>("principal")

        val role = claims.getClaimValueAsString("role")
        val scope = claims.getClaimValueAsString("scope")

        val data = DataResponse("API Request has role: $role and scope: $scope")
        return Gson().toJson(data)
    }
}

Test the API

The API’s clients will call it via HTTP requests with an access token in the Authorization header, and this can be easily tested via a CURL request:

curl -i http://localhost:3000/api/data -H "Authorization: Bearer eyJraWQiOiIyMTg5NTc5MTYiLC ..."

In the event of a problem validating the JWT, an error reason can be returned to the client. The sample returns a generic error in the WWW-Authenticate response header, though a custom response object could be returned instead:

HTTP/1.1 401 Unauthorized
Date: Mon, 12 Apr 2021 16:01:07 GMT
WWW-Authenticate: Bearer, error=invalid_token, error_description=Access token is missing, invalid or expired
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Server: Jetty(9.4.31.v20200723)

Logging and Error Handling

The sample API also shows how to capture jose4j internal log messages and how to work with its error objects. In real world APIs you will want to quickly diagnose any reasons for token validation failures or problems connecting to the Authorization Server.

catch (e: InvalidJwtException) {

    _logger.info("JWT validation failed")
    for (item in e.errorDetails) {
        _logger.debug("${item.errorCode} : ${item.errorMessage}")
    }

    this.unauthorizedResponse(httpResponse)
}

Conclusion

Integrating the jose4j library is an easy way to implement OAuth security in Java or Kotlin APIs. Once done you have full access to the access token’s scopes and claims, so that your API can secure access to its data.

Keep up with our latest articles and how-tos RSS feeds.