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
The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.
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:
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:
port=3000jwks_endpoint=https://idsvr.example.com/oauth/v2/oauth-anonymous/jwksissuer=https://idsvr.example.com/oauth/v2/oauth-anonymousaudience=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:
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.
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:
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
:
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:
mvn packagejava -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:
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/1.1 401 UnauthorizedDate: Mon, 12 Apr 2021 16:01:07 GMTWWW-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.
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