Create a Secure REST API with Taffy and JWT Authentication

REST APIs are a popular way to expose the functionality of a web application to other systems through the internet. They enable other systems to interact with your application using HTTP requests and receive responses in a standard format. In this guide, we will create a REST API using the Taffy framework and the JWT-CFML library for authentication in the Lucee CFML engine.

In this example, we will use Docker and Docker Compose to run the application. The CFML engine is running inside a CommandBox container, but you can also use CommandBox directly or any other Lucee installation. Note that while this example is specifically tailored for Lucee, it may also work on other engines with some adjustments.

You can find a working example of this guide on my GitHub page:

GitHub - rabume/taffy-jwt-api
Contribute to rabume/taffy-jwt-api development by creating an account on GitHub.

Setting Up Taffy

To set up Taffy, we first need to download the latest release from GitHub. If you want to create an API for an existing application, create a subfolder in which Taffy will reside. For the purposes of this tutorial, we will name this subfolder "api". Clone or unpack Taffy in this subfolder.

We only need the following files/folders from Taffy for this example:
- /bonus/*
- /core/*
- /dashboard/* (optional for development)
- /examples/* (optional for development)

You can delete everything else. For testing purposes, you can use the Taffy dashboard or a tool like Postman.

Next, create an empty "index.cfm" file. This file is necessary for Taffy to work and should be left blank. All requests are funneled through it and handled by the framework's internal functionality.

After that, create the "application.cfc" file for Taffy. For now, we will add the basic Taffy configuration and a array containing some books.

component extends="taffy.core.api" {

    // Add mappings for taffy
    this.mappings["/resources"] = expandPath("./resources");
    this.mappings["/taffy"] = expandPath("./taffy");
    
    // Set taffy framework settings
    variables.framework = {
        reloadKey = "reload",
        reloadPassword = "aVerySecurePassword",
        reloadOnEveryRequest = false,
        disableDashboard = true, // Set to false if you want to use the dashboard.
        disabledDashboardRedirect = "/",
    }; 
    
    function onApplicationStart() {

        // Set timezone
        setTimezone("UTC+00:00");

        /*     
            Defines a array that stores data in a way that 
			is not persistent across application restarts or 
            reinitializations. In a production environment, 
            it would be more appropriate to store this data 
            in a database or other persistent storage solution 
            to ensure its availability beyond a single execution
            of the application.
        */
        application.books = {
            "books": [
                {
                    "name": "Harry Potter and the Goblet of Fire",
                    "author": "J.K. Rowling",
                    "pages": "734"
                },
                {
                    "name": "Harry Potter and the Order of the Phoenix",
                    "author": "J.K. Rowling",
                    "pages": "912"
                },
                {
                    "name": "The Lord of the Rings",
                    "author": "J.R.R. Tolkien",
                    "pages": "1216"
                },
                {
                    "name": "The Fellowship of the Ring",
                    "author": "J.R.R. Tolkien",
                    "pages": "432"
                },
            ]   
        }

        // Needed to not completly override the onApplicationStart() from taffy
        super.onApplicationStart();
    }

}
application.cfc

To create resources that can be served by your API, create a folder called "resources" and a file called "getBooks.cfc" inside it.


component extends="taffy.core.resource" taffy_uri="/getAllBooks" {

    function get(){
        return rep(application.books).withStatus(200);
    }
}
getBooks.cfc
⚠️
To access the API, you can use the endpoint "/api/index.cfm/getAllBooks". If you want to remove "index.cfm" from the endpoint, you will need to add a rewrite rule to your web server. You can find more information on this in the taffy documentation.

Add JWT Authentication

Now, we can begin implementing JWT-based authentication for our API. While Taffy offers other options like basic authentication, we will focus on using JSON Web Tokens for this guide.

To get started, we need to download the JWT-CFML library from the latest release on GitHub. Once the download is complete, unpack the contents into a subfolder where your API is located. I recommend to create a folder specifically for the JWT-CFML library, such as "jwt". This will help keep your API organized and make it easier to reference the library in the future.

Now, we can initialize the JWT library and create the "onTaffyRequest()" function to handle the decoding and validation of the JSON Web Tokens. I have also included a hardlink to the authentication route for your reference.

component extends="taffy.core.api" {
	
    // Note: In the example available on my GitHub page, 
    // I have stored these values/keys in a configuration 
    // file. However, for the purposes of this code snippet, 
    // I have chosen to hardcode them here.
    
	// Set global variables
    application.apiSecret = "VerySecureSecret" ;
    application.issJWT = "IssuerName";
    application.apiKey = "apiKeyYouShouldNeverHardcodeEspeciallyNotHere";

    // Add mappings for taffy
    this.mappings["/resources"] = expandPath("./resources");
    this.mappings["/taffy"] = expandPath("./taffy");

    // Set taffy framework settings
    variables.framework = {
        reloadKey = "reload",
        reloadPassword = "aVerySecurePassword",
        reloadOnEveryRequest = false,
        disableDashboard = true, // Set to false if you want to use the dashboard.
        disabledDashboardRedirect = "/",
    }; 

    function onApplicationStart() {

        // Set timezone
        setTimezone("UTC+00:00");

        // Initialize jwt-cfml
        application.jwt = new jwt.models.jwt();

        /*     
            Defines a array that stores data in a way that 
            is not persistent across application restarts or 
            reinitializations. In a production environment, 
            it would be more appropriate to store this data 
            in a database or other persistent storage solution 
            to ensure its availability beyond a single execution
            of the application.
        */
        application.books = {
            "books": [
                {
                    "name": "Harry Potter and the Goblet of Fire",
                    "author": "J.K. Rowling",
                    "pages": "734"
                },
                {
                    "name": "Harry Potter and the Order of the Phoenix",
                    "author": "J.K. Rowling",
                    "pages": "912"
                },
                {
                    "name": "The Lord of the Rings",
                    "author": "J.R.R. Tolkien",
                    "pages": "1216"
                },
                {
                    "name": "The Fellowship of the Ring",
                    "author": "J.R.R. Tolkien",
                    "pages": "432"
                },
            ]   
        }

        // Needed to not completly override the onApplicationStart() from taffy
        super.onApplicationStart();
    }

    // This function is called after the request has been parsed and all
	// request details are known
    function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers){
        
        if(arguments.cfc == "authenticate") {
            return true;
        }
        
        // Check if authorization header exists
        if(!structKeyExists(arguments.headers, "Authorization")){
            return noData().withStatus(401);
        }
        
        // Remove bearer keyword and parse only token
        local.token = trim(ReplaceNoCase(arguments.headers.Authorization, "Bearer", ""));

        // Check if token is valid
        try {
            local.decodedJWT = application.jwt.decode(local.token, application.apiSecret, "HS256")
            local.isValidToken = true;
        }catch(any e) {
            local.isValidToken = false;
        }

        // Return 401 when token isn't valid
        if(!local.IsValidToken){
            return noData().withStatus(401);
        }

        return true;
    }
}
application.cfc

Now, we will create the authentication resource where users of the API can request tokens for their own use in identifying themselves to the system.

component extends="taffy.core.resource" taffy_uri="/authenticate/{apiKey}" {

    function get(required string apiKey){

        // Check if the provided API key is valid
        local.apiKeyValid = application.apiKey == arguments.apiKey;

        // Return 401 when api key isn't valid
        if(not local.apiKeyValid){
            return noData().withStatus(401);
        }

        // Set the lifetime of the token (60 seconds)
        local.validUntil = dateAdd("s", 60, now())
        local.startDate = createdatetime( '1970','01','01','00','00','00');
        local.unixStamp = datediff("s", local.startDate, local.validUntil);

        local.payload = {
            "iss": application.issJWT,
            "iat": now(),
            "exp": local.unixStamp
        }

        // Encode token
        local.enocodedJWT = application.jwt.encode(local.payload, application.apiSecret, 'HS256');

        // Send token
        return rep(["token" : local.enocodedJWT]);
    }

}

authenticate.cfc

Now, you should be able to request a token through the authentication route by providing the correct API key. This token can then be used to authenticate yourself and access the "getAllBooks" resource.

In conclusion, we have successfully implemented REST API functionality using the Taffy framework and JWT-CFML library for authentication in the Lucee CFML engine. We set up resources that can be accessed through specific endpoints, and added an extra layer of security with JWT-based authentication. Don't forget to check out the working example on my GitHub page for more details and to see everything in action. Thanks for following along!

If you need any assistance or have suggestions for improvement, please leave a comment or contact me via email.

Resources

GitHub - atuttle/Taffy: The REST Web Service framework for ColdFusion and Lucee
:candy: The REST Web Service framework for ColdFusion and Lucee - GitHub - atuttle/Taffy: The REST Web Service framework for ColdFusion and Lucee
GitHub - jcberquist/jwt-cfml: CFML (Lucee and ColdFusion) library for encoding and decoding JSON Web Tokens.
CFML (Lucee and ColdFusion) library for encoding and decoding JSON Web Tokens. - GitHub - jcberquist/jwt-cfml: CFML (Lucee and ColdFusion) library for encoding and decoding JSON Web Tokens.