JSON Web Tokens (JWT) are commonly used in single-sign-on solutions. They can also be used to authenticate single-page front-end applications with a back-end API. The benefit is that they are lightweight and can be sent with every request so they are stateless. That means server side sessions are not necessary. This makes scaling an application easier as well.
Awhile back I created a CFML component named cf-jwt-simple that creates and verify’s JSON Web Tokens. It was a port of a Node.js library to CFML. In this post I will give a example of using it to do authentication to a back-end api.
cf-jwt-simple requires a secret key that is used to signing and verification. I will add the key as a setting in config/Coldbox.cfc. Remember to change the secret key to some something more secure than this. Use something random and long as the key should be secure and unique to your application.
I will also add a setting the token expiration time.
I add a wirebox mapping for the JWT in config/Wirebox.cfc. This will initialize the JWT library with the secret key that was setup in the previous step. See the Wirebox Documentation for more info in setting up bindings.
Here I will use CommandBox to create a new model component named AuthenticationService.cfc
I then inject the JWT component into AuthenticationService using wirebox.
I also inject the expiration setting.
I then implement a method named validateUser that takes a username and password. It should lookup the username and use a password hashing algorithm to check that the hashed password matches the one stored in your database. Secure credential storage and password hashing is out of the scope of this tutorial. Here is a couple good reasources on doing this in CFML.
Next I create the grantToken method. It will return a fresh JWT with the userid as the subject. First I create a structure named payload that contains three reserved JWT claims. One of the goals of JWT is to be compact so they use three letter designation for the claims. “iss” stands for issuer. It is used to identify who issued the token. The “exp” claim stands for expiration. Here I use the tokenExpiration setting to set the expiration timestamp. The “sub” claim stands for subject. I am assigning the userid as the subject but it could be some other data as well. You can also add our own custom claims as well that can be accessed with every request. Be careful not to store any sensitive information unless you plan to use an encrypted JWT. cf-jwt-simple does not handle encryption. If you need encryption it may be better to use one of the Java libraries listed here. Finally the token is encoded and signed using the HmacSHA512 algorithm.
Then I create a method to validate our token when recieved from the client. First I try to decode the token. This will return the payload as a structure. If the signature is invalid it will throw an error which is why it is wrapped in a try-catch block. If the data structure exists we know the token signature was valid. Next I check the expiration to see if it is still within its validity period.
I also add a method to decode the token and return the data encoded within the token
Here I use CommandBox to create a new handler component named Authenticate.cfc. It will be responsible for handling authentication requests.
I then modify the handler to extend BaseHandler. I also restrict the request to the http POST method by setting the this.allowedMethods property. Finally I modify the index action to validate the username and password using the authService. It will return an access token if the authentication is successful. I inject the authService into the BaseHandler next so no need to do it here.
Then I modify handlers/BaseHandler.cfc and inject the AuthenticationService.
Before the action is executed in the aroundHandler method I add the following code snippet to check the authentication token for each request.
Before the authentication code I also add some code to enforce JSON content for POST and PUT methods. It will deserialize the JSON and merge it into the rc scope.
Last I create some tests to verify that it is working. In CommandBox run the following.
Here is the test suite I used verify that it is working.