Generating JWTs in native FileMaker

API, cURL, FileMaker, JWT

Generating JWTs in native FileMaker

Today a colleague asked me to take a look at an issue they were experiencing interacting with the Zoom API (I’ll write about that another day), but in the conversation they asked if I knew if it were possible to create the JSON Web Token (JWT, pronounced jot) used by the API in native FileMaker.

What is a JWT I hear you ask? According to jwt.io (which we’ll come back to later):

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

Say what? In simple terms, they are a way of authenticating when using an API. Other options include OAuth, API keys, Basic Auth etc, but JWT has an inherent simplicity which makes them pretty popular.

Essentially they are comprised of three parts, a header, a payload and a signature. The first two of those are JSON objects, and the third signs the first two. All three parts are then encoded and concatenated with a full stop (period to those in the US). When all’s said and done they look something like this:

eyJhbGciOiJIUzI1NiIsInR5cGUiOiJKV1QifQ.eyJleHAiOjE1OTM2MTYyNTMsImlzcyI6IlpPT01fQVBJX0tFWSJ9.8t65HsD_CSoHZadlaF7xxQRayb3URrX6OaVr-rojB7A

Let’s take the parts one at a time.

Header

The header is the same for all JWTs, no matter what the payload might be, it’s a JSON object which defines the algorithm which will be used to sign the JWT, and the type of token it is – always JWT when working with a JWT. The Zoom API (and many others) use HMAC SHA256 which is expressed as HS256, so your header looks like this.

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

You’ll be unsurprised to learn that the payload is also a JSON object. In the case of Zoom it’s pretty simple.

{
   "iss": "ZOOM_API_KEY",
   "exp": 1496091964000
}

What goes in there is entirely up to the API that you’re interacting with. In this case they want your API key, and an expiry timestamp that the JWT expires at.

This needs to be a Unix timestamp some time in the future. Best practice says it should be very short-lived so that if it’s captured ‘in transit’ it can’t be used for very long. You should really be issuing a new JWT immediately before you make any API call that’s valid only for a few seconds.

A Unix timestamp is the number of seconds which have passed since 00:00 on the 1st of January 1970 UTC. So as I’m typing this at 1446 BST on the 1st of July 2020 the current value is 1593611229. FileMaker on the other hand starts counting way before then, in fact it’s counting the number of days since the 1st of January 0001. So to convert from one to the other:

_timestamp = GetAsNumber ( Get ( CurrentTimestamp ) ) - 62135596800

Signature

The signature is where things to start to get a bit trickier. The first thing you need to do is to Base64 encode the header and the payload. FileMaker has a couple of functions to take care of that for us, but they can’t quite do exactly what we need on their own because it has to be URL Base64 Encoding, and any padding (represented by an = in a Base64 encoded string) has to be removed.

Fortunately dealing with that’s not too tricky – you just have to know to do it, and what to do. You also need to make sure you use the Base64EncodeRFC version of FileMaker’s Base64 encoding, and specify RFC 4648 – this ensures there are no line breaks at the end of the encoding. So the encoding ends up being:

_encodedHeader = Substitute ( 
   Base64EncodeRFC ( 4648 ; _header ) ;
   [ "=" ; "" ] ; [ "+" ; "-" ] ; [ "/" ; "_" ]
) ;

The substitute is stripping off the padding (represented by the =, then replacing + with - and / with _. The last two are necessary because those characters have special meaning in a URL, so can’t be present.

Once we have both the header and the payload correctly encoded we concatenate them to form the body of our JWT.

_body = _encodedHeader & "." & _encodedPayload ;

Which we then sign:

_signature = CryptAuthCode ( _body ; "SHA256" ; _secret )

Finally we have to then perform the same encoding of the signature as we did the other two parts of our JWT, then add it to the end. In its entirety the generation of a JWT in a single Let statement looks like this:

Let ([
    _secret = "ZOOM_SECRET_KEY" ;
    _apiKey = "ZOOM_API_KEY" ;

    _lifespan = 60 ;
    _timestamp = GetAsNumber ( Get ( CurrentTimestamp ) ) - 62135596800 + _lifespan ;

    _header =  JSONSetElement ( "{}" ; [ "alg" ; "HS256" ; JSONString ] ; [ "type" ; "JWT" ; JSONString ] ) ;
    _payload = JSONSetElement ( "{}" ; [ "iss" ; _apiKey ; JSONString ] ; [ "exp" ; _timestamp ; JSONNumber ] ) ;

    _encodedHeader = Substitute ( 
        Base64EncodeRFC ( 4648 ; _header ) ;
        [ "=" ; "" ] ; [ "+" ; "-" ] ; [ "/" ; "_" ]
    ) ;
    _encodedPayload = Substitute ( 
        Base64EncodeRFC ( 4648 ; _payload ) ;
        [ "=" ; "" ] ; [ "+" ; "-" ] ; [ "/" ; "_" ]
    ) ;

    _body = _encodedHeader & "." & _encodedPayload ;
    _signature = Substitute ( 
        Base64EncodeRFC ( 4648; CryptAuthCode ( _body ; "SHA256" ; _secret ) ) ;
        [ "=" ; "" ] ; [ "+" ; "-" ] ; [ "/" ; "_" ]
    )
    ];
  _body & "." & _signature
)

If it were me, I’d be creating a custom function called JWTBase64Encode to take care of specialist encoding which is required for JWTs, but for portability, that single Let statement does the job well.

It’s possible that other APIs will require a different algorithm – there are many others which are used for JWTs – if CryptAuthCode supports that which is required then it’s a matter of updating the Header and the CryptAuthCode function. If it doesn’t, then you’re going to have to resort to using a plugin or JavaScript (for example if RS256 is required) to do the signing.

If you need to test that you’re generating JWTs correctly then the testing panel on jwt.io is super helpful for that.

Leave A Comment

*
*