JWT Functions as a Service with Fly.io

Almost every week I read online about how FaaS [Functions as a Service] or serverless is the future of cloud computing. The premise is simple - you write some piece of code and add a trigger point to it - and everything else such as deployment, metrics, scaling etc are all taken care of. I generally would avoid it for anything more than submitting forms from a frontend [probably why Node.js is almost always supported] or basic REST middleware anything more involved than that is pushing it.

Things like Lambdas or Cloud functions are great for what they are but you are constrained to the runtime environments that are supported. It would be great if I could deploy a docker container instead, which would give me a lot more flexibility. What sucks also is the cold startup time. If your function is not receiving traffic - it will scale down to zero and such you will pay a penalty for when the next request comes in. If you are putting a JVM based app this can be quite unbearable due to the warmup period during which response times are slower [which is lost every time the app scales down to 0].

A little while back, I read about fly.io on Hacker News and was quite intrigued. Three commands and your app is deployed. Think of fly.io as a CDN for your docker images. You can deploy your image, expose a port and fly will set up a DNS that routes to that port from 443 for you and it won't scale down to 0 during low traffic periods.

// install flyctl
brew install superfly/tap/flyctl

// deploy your image
flyctl deploy

// scale to multiple regions
flyctl scale regions ams=1 hkg=1 sjc=1

I wanted to try it out and see how easy it would be to run something like this. They also give $10/month service credit to allow for hobbyist devs to experiment with. I decided to deploy JWTs as a service on fly.

The stack is simple. Kotlin as the language. Vert.x as the framework. Auth0 JWT library to create and validate the JWTs. Jib to create the docker image and OpenJ9 as the JVM.

The base URL for this service is: https://jwt-service.fly.dev

The two main routes are POST /api/v1/token to generate a new token and GET /api/v1/validate?token=<token> to validate the given token.

The POST request takes a JSON body in the following format

{
    // user which the JWT is being issued for
    "sub": "funny_username420",
    // when should this token expire - epoch
    "expires_at": 1589583640,
    // data you want to embed into the token - things like shard ID etc
    "claims": {
        "experiment": "control"
    }
}

The API is quite forgiving around the body and has sane defaults as a fallback. If you do not want to open up Postman to make the POST request - you can also use the debug route which generates a JWT valid for 5 minutes from GET /debug/api/v1/token

Making the above request returns back

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmdW5ueV91c2VybmFtZTQyMCIsImV4cGVyaW1lbnQiOiJjb250cm9sIiwiaXNzIjoiand0LXNlcnZpY2UiLCJleHAiOjE1ODk1ODM2NDB9.z628OOjvG8RzeWEImD2WVA1XLJhbTPFiN1f0dU7It5A

Paste the token in jwt.io to see what the payload is. JWTs are signed but not encrypted [they can be] so you can see what has been added to them. Therefore, never add sensitive information such as passwords to the token.

To validate the above token we use the second route, which will return

{
  "result": "OK",
  "data": {
    "iss": "jwt-service",
    "sub": "funny_username420",
    "experiment": "control",
    "exp": "1589583640"
  },
  "error": ""
}

If the token were to be manipulated with. let's say by changing the subject, the validation will fail.

{
  "result": "ERROR",
  "data": {
  },
  "error": "The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"
}

I timed myself on how long it took me to build and deploy the image and it was less than one minute - not too bad. The deploy steps were

mvn compile jib:dockerBuild
docker image ls
flyctl deploy -i <image-id>

I am using the ID since I am using jib to create a docker image instead of a docker file. If you have a docker file it will pick it up automatically from the current directory.

Looking at the code that is running - it's pretty straight forward.

// init vertx
val vertx = Vertx.vertx()

// create router
val router = Router.router(vertx)

// add a x-response-time header so we can see how long the server took to process the request
router.route().handler(ResponseTimeHandler.create())

// log each request
router.route().handler(LoggerHandler.create())

// allow easy decoding of JSON Bodies to data class objects
router.route().handler(BodyHandler.create())

The JSON body can be converted to a Kotlin data class in one line using val generateTokenRequest = Json.decodeValue(it.body, GenerateToken::class.java)

This is how the token is created, signed and returned in the response.

val algorithm = Algorithm.HMAC256("very-private-secret-key")

router.get("/debug/api/v1/token").handler {
        val token = JWT.create()
            .withIssuer(iss)
            .withSubject("dummy-subject")
            .withClaim("experiment_group", "control")
            .withExpiresAt(Date.from(Instant.now().plusSeconds(300))) // 5 minutes
        it.response().end(token.sign(algorithm))
    }

When you are doing REST-style stuff and don't have strict definitions like Protobuf - it can be tedious to do a lot of validations on the request that comes in. Thankfully, Vert.x provides out of the box support for validations using its Web API contract module. For the validate route, the token query param is required - and all I have to add is this one line validation handler that takes care of the entire thing.

 val validationHandler = HTTPRequestValidationHandler.create().addQueryParam("token", ParameterType.GENERIC_STRING, true)

 router.get("/api/v1/validate")
        .handler(validationHandler)
        .handler { // actual validation code }

When verifying a JWT there can be exceptions thrown if it were invalid or expired. The goto way to deal with that would be try/catch. I still see Kotlin devs using try/catch like they are writing Java - but that feels very unnatural to the language. A better approach is to use runCatching - something like the following

runCatching {
  val jwt: DecodedJWT = verifier.verify(token)
  // verify the token
  // return response 
}.onFailure { throwable ->
  // handle exception
}.onSuccess { responseData ->
  // handle success
  it.response().setStatusCode(200).end()
}

Start the server as following

vertx.createHttpServer().requestHandler(router).listen(8080)

The jib maven plugin super simple to add to your pom file - here is a snippet from mine

<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>1.8.0</version>
<configuration>
  <from>
    <image>adoptopenjdk/openjdk11-openj9:alpine-jre</image>
  </from>
  <to>
    <image>jwt-service</image>
  </to>

And there you go - a fully functioning JWT middleware deployed in almost no time. The full list of supported endpoints are:

Base URL: https://jwt-service.fly.dev/

// health check
GET /ping

// get memory usage and available processors
GET /debug/runtime
GET /debug/api/v1/token

POST /api/v1/token
GET /api/v1/validate?token=<token>

Edit: The deploy is so fast and painless that I went back and added the link to this article on the base route at https://jwt-service.fly.dev/. Hmm, there is potential here.

No Comments Yet