/images/resources/code-examples/code-examples-kotlin.jpg

Securing a Kotlin API with JWTs

On this page

Overview

This tutorial explains how to integrate the jose4j security library and use it to validate JWTs in a plain Kotlin API. The minimal Spark Java REST API stack is used.

Note

Curity Identity Server is used in this example, but other OAuth servers can also be used.

Create a Kotlin API

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

kotlin
12345678910111213
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 example 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:

1234
port=3000
jwks_endpoint=https://idsvr.example.com/oauth/v2/oauth-anonymous/jwks
issuer=https://idsvr.example.com/oauth/v2/oauth-anonymous
audience=api.example.com

Security Library Integration

The jose4j library performs the detailed security work. When the OAuth filter is created it constructs global objects to manage downloading JWKS keys:

kotlin
12
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 a JWT access token having the same kid value, do not trigger additional HTTPS calls to the JWKS endpoint, so that API performance is good.

The token validation behavior is configured as follows, where allowed algorithms are restricted to those expected from clients, as recommended in JWT Security Best Practices.

kotlin
123456789
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. This is then attached to the request object:

kotlin
12
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:

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

kotlin
12345678910111213
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)
}
}

Run the API

To build and run the API, first ensure that maven and a JDK of version 17 or higher are installed. Then run the following commands, which will run the API on port 3000:

bash
12
mvn package
java -jar target/secureapi-1.0-SNAPSHOT-jar-with-dependencies.jar

Test the API

Clients will call the API using HTTP requests, with an access token in the Authorization header. To do so, configure a client as described in the getting started guides, then use OAuth tools to get an access token. You can then send the token to the API using a cURL request:

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

In the event of a problem validating the JWT, an error reason is returned to the client. The example returns a generic error in the WWW-Authenticate response header:

http
123
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

Logging and Error Handling

The example API also shows how to capture jose4j internal log messages and how to work with its error objects. In real world APIs this would enable any technical failures to be diagnosed, such as connecting to the authorization server.

kotlin
123456789
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 apply business rules, to secure access to its data.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial