Coldbox and VueJS untangled

Cbsecurity (4): JSON Web Tokens (JWT)

I ‘ve been using cbsecurity V1 for a long time. When we switched from a coldbox application to a VUE frontend and coldbox powered API backend we had to revise our authentication requirements. We didn’t want sessions anymore se we needed something which could be sent with each request to provide our authentication.

In this post I will discuss everything needed for a cfml API which is secured with cbsecurity v2.x. I’ll start with some general JWT info, followed by sample code.

JWT concepts

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

https://jwt.io/introduction/

JWT is very useful in the following scenarios:

  • Authorization: the most common scenario for using JWT. JWT can contain all kind of user information in a compact token, which has to be send with every request. With this token a user can be granted access to all kind of resources.
  • Information Exchange: JSON Web Tokens are a good way of securely transmitting information. Tokens can be signed and because header and payload are part of the signature you can very the content hasn’t been tampered with. By using public/private key pairs you can verify the sender.

A typical JWT looks like this:

xxxxx.yyyyy.zzzzz : a header, payload and signature part. A lot more information, including a debugger can be found at jwt.io.
A JSON Web Token encodes a series of so called claims in the payload part of your token.

CbSecurity v2 has support for JWT built in. It will assist you with generating, encoding, decoding and security aspects of JSON Web Tokens. It shields you from a lot of the complexities of JWT, so with some configuration and a few standard files you will be ready. But before we build something, I will explain some basic concepts.

Base claims

Before we continue, let’s explore the base claims that Coldbox Security JWT will create for you. A nice overview can be found in the cbsecurity manual .

  • Issuer (iss) – The issuer of the token (defaults to the application’s base URL)
  • Issued At (iat) – When the token was issued (unix timestamp)
  • Subject (sub) – This holds the identifier for the token (defaults to user id)
  • Expiration time (exp) – The token expiry date (unix timestamp)
  • Unique ID (jti) – A unique identifier for the token (md5 of the sub and iat claims)
  • Scopes (scope) – An array of scopes attached to the token

Scopes

One thing you have to ask yourself is: should I use scopes? So what are these so-called scopes?

Scopes are a sort of tags which describe what you are allowed to do in an application, so they could be translated as permissions or roles. A standard use case in an API: The calling application is a third-party, or external, application. In this case, the calling application will request authorization from the user to access the requested scopes, and the user will approve or deny the request.
So this way the user defines what access is allowed for the third party and this will be reflected in the token in the scope claim.
So the third party will request access with a list of requested scopes, and will be returned a JWT with a list of allowed scopes. The scope claim will be a space delimited string of allowed scopes, e.g.

scope : "profile read:email delete:images"

In many case you are not creating access for a third party, but you just need to access an API from your own client app. You can ask yourself if you want to specify scopes in your token in this case. Cbsecurity will do TWO different authorization checks: first it will check if required permissions are in your scope. For this it has to parse your token, but no access to your authorization backend is required anymore, but you have to realize your security scopes will be visible since a JWT can not be modified, but is readable. The second authorization check will call hasPermissions() on your user object. This is all behind the scenes, so it boils down to the question: does my client app need to know these permissions? If not, and if you don’t need third-party access, there’s not much reason to use scopes.
This Stackflow discussion gives some more arguments about (not) using scopes.

Custom claims

Sometimes you might want to store more information in your JWT. We want to have the username available for example, so our client app can find the username in the JWT without going back to the API. It’s up to you which custom claims you want to use.

JWT sample code

I will show here what’s needed to create a cfml API protected by a JWT authentication and authorization system. We will use cbauth for authentication which I discussed in part 2 of this series.

Configuration

I start by adding the necessary configuration in the modulesettings of my coldbox configuration.

moduleSettings = {
  "cbAuth" : {
    userServiceClass: "UserService"
  },
  cbSecurity : {
    //"authenticationService"  	   : "authenticationService@cbauth",
    "validator"	                   : "JWTService@cbsecurity",
    "userService"                  : "UserService",
    "invalidAuthenticationEvent"   : "v1:echo.onAuthenticationFailure",
    "defaultAuthenticationAction"  : "override",
    "invalidAuthorizationEvent"    : "v1:echo.onAuthorizationFailure",
    "defaultAuthorizationAction"   : "override",
    "useSSL"                       : true,
    "handlerAnnotationSecurity"    : false,
    "rules"                        : "model",
    "rulesModel"                   : "SecurityService",
    //"rulesModelMethod"           : "getSecurityRules",
    "jwt"                     	   : {
      //"issuer"                     : "",to use
      "secretKey"                    : getSystemSetting( "JWT_SECRET", "" ),
      //"customAuthHeader"           : "x-auth-token",
      "expiration"                   : 24*60,
      //"algorithm"                  : "HS512",
      "requiredClaims"               : ["iss","sub","exp","scope"] ,
      "tokenStorage"                 : {
        "enabled"                      : false,  
        //"keyPrefix"                  : "cbjwt_",
        //"driver"                     : "cachebox",
        //"properties"                 : {
          //"cacheName"                  : "default"
        //}
      } 
    }
  },
// more ...
}

Since we are using cbauth, I have to specify a userServiceClass for cbauth. This userservice needs some interface methods as discussed in part 2. (I will show the methods later in this post as wel). Next we need to configure quite a lot of properties for cbsecurity:

  • authenticationService: cbauth is the default so you don’t have to add this.
  • userService: this is the same userService as specified in cbauth. We will discuss required methods later.
  • The invalidAuthentication and invalidAuthorization events are simple. We use the REST HMVC template which has these methods built in the basehandler, so we can specify them on any handler wich is derived from the REST basehandler. Coldbox 6 even has this baseHandler built in. In a REST interface you always need override as default action for both authentication and authorization.
  • useSSL: we force it to true. But if your API is already SSL-only, you don’t have to worry about this setting.
  • handlerAnnotationSecurity: since we don’t use annotations but the more flexible rules as discussed in part 3 we set this to false.
  • rules: This is an important one! You always have to specify rules, even if you use a rulesDatabase or rulesModel with additional properties. In our case we use a rulesModel so rules="model" . To be honest I see no reason to use inline rules or a rulesDatabase, because a rulesModel is more flexible. In a model you can define an array of structs for rules, or read them from a database or json file and do some postprocessing, such as adding some deny-all rule or filtering or sorting. The model approach fits all other options, and even if you decide to only have an array of structs now, it is trivial to create a service which just returns an array of structs
  • rulesModel and rulesMethod: I created a securityService which returns an array of rules in a rulesMethod= "getSecurityRules"
  • jwt.issuer: if you don’t specify an issuer the value from event.buildLink() will be used. The validators will check if the token was created by the same issuer.
  • jwt.secretKey: You don’t have to specify a secretkey, because cbsecurity will generate a random one automatically. But if you restart your application all JWT tokens become invalid. By specifying a fixed secret, which can be stored in an environmental variable your keys remain valid, which is especially handy in a development environment.
  • by default cbsecurity will look for a bearer token in the authorization header, but it can also look in another header as specified here x-auth-token by default. Finally cbsecurity will look in the request scope for a simularly named variable.
  • jwt.expiration: default expiration is 60 minutes. but we increased it to one day. If you have very high security requirements you should keep your expiration time low and refresh often.
  • jwt.algoritm: we used the default but many more options are available as described in the manual.
  • jwt.requiredClaims: this is also about security. You can use any claim you want but by defining them as required you make sure your JWT is only valid if it contains the required claims.
  • jwt.tokenStorage: there can be a lot of discussion if you need a tokenStorage. If you have one, each request has to be validated against this storage and if it is not there your token will be rejected. If you don’t have a tokenStorage there is no way to keep users out if they have a valid token. There are several options for tokenstorage such as cache, db and your own solution. If you use the default memory cache it will be cleared on application reset, so your tokens will become invalid, which is inconvenient in a development environment. If you don’t use tokenstorage, your tokens remain valid until they expire. It is minimally faster, but less secure because you loose the ability to block certain tokens. If you are working with permissions for your user, you could still revoke permissions to restrict access, but if you keep your permissions in a scope claim, there’s no way to stop them. In our scenario we leave the scope empty and check our permissions from the user object.
    NOTE: There’s a bug in cbsecurity versions <= 2.6.0 which prevents disabling the tokenStorage, so if you need that, make sure you install a more recent version

UserService

We are using cbauth which needs a UserService. The same UserService can be used for JWT and specified in the UserService property. This userService has to implement the cbsecurity.interfaces.IUserService.cfc interface, which means at least the following three methods should be present.

	/********************************************************************************/
/********* METHODS FOR cbsecurity.interfaces.IUserService  *********************/
/********************************************************************************/
/**
 * Verify if the incoming username/password are valid credentials.
 *
 * @username The username
 * @password The password
 */
boolean function isValidCredentials( required username, required password ){
  var q = wirebox.getInstance("QueryBuilder@qb")
    .from("users")
    .where("Name", arguments.UserName)
    .andwhere("Password", arguments.password)
    .first();
  return !q.isEmpty();
}
/**
 * Retrieve a user by username
 *
 * @return User that implements JWTSubject and/or IAuthUser
 */
function retrieveUserByUsername( required username ){
  var sUser = wirebox.getInstance("QueryBuilder@qb").from("users")
    .where("name", arguments.username)
    .first();
  return get( sUser.id );
);

/**
 * Retrieve a user by unique identifier
 *
 * @id The unique identifier
 *
 * @return User that implements JWTSubject and/or IAuthUser
 */
function retrieveUserById( required id ){
  return get( arguments.id )
};

As you can see the isValidCredentials method is just checking for a user with name and a certain password. In a production environment your function will be less simplistic because there should be some strong encryption present for your password field. This also makes the validation more complex. In a next installment I will show some example how to do this with bcrypt. Our userservice also has a get(userId) method which returns a User object. The retrieveUserByName and retrieveuserById are simple wrappers for this method.

User object

Our User object has to implement two interfaces, which are 5 methods together.

component accessors="true" {
	
// DI
property name="auth" inject="authenticationService@cbauth";
// Properties
property name="id" type="string";
property name="name" type="string";
property name = "password" type="string";
property name = "permissionList" default="";
	/********************************************************************************/
/********* METHODS FOR cbsecurity.interfaces.IAuthUser.cfc  *********************/
/********************************************************************************/

//implicit getId from id property
    
boolean function hasPermission( required permission ){
  return arguments.permission.listToArray().some((item) => {
    return listFindNoCase( this.getPermissionList(), item );
  })
}

boolean function isLoggedIn(){
  return auth.isLoggedIn();
};
	/********************************************************************************/
/********* METHODS FOR cbsecurity.interfaces.jwt.IJwtSubject.cfc ****************/
/********************************************************************************/
/**
 * A struct of custom claims to add to the JWT token
 */
struct function getJwtCustomClaims(){
 return {
    "nam": this.getName(),
  }
};

/**
 * This function returns an array of all the scopes that should be attached to the JWT token that will be used for authorization.
 */
array function getJwtScopes(){
  return []
  //return listToArray(this.getpermissionList());
};

}

The getId is trivial because there already is an id property, and the isLoggedIn function is just a shortcut to the authentication service. but the naming of the hasPermission is tricky, because it suggest you have to check ONE permission. Looking at the interface description this is not the case:

* Verify if the user has one or more of the passed in permissions
* * @permission One or a list of permissions to check for access

So you have to process every element from a LIST of permissions.

As you can see I only added one custom claim, but our real-life scenario has more, so feel free to invent your own claims.
Regarding scopes: we don’t use them so my getJwtScopes just returns a simple empty array. I could have added all permissions for my user to the scopes, but in our system that would add up to many different scopes, because we have a very granular permissions system.

Rules and permissions

We defined a securityService which does nothing else but returning an array of rules. I’ll show some of these rules to get an idea how you could organize your own

getSecurityRules(){
    //create all securityRules first
    var whiteRules = [
        "ApiKeys.create",
        "v1:Main",
        "echo",
        "v1:auth.Users\.requestPasswordReset",
        "v1:auth.Users\.resetPasswordFromToken"
    ];
    var aRules = [	
        //authorization
        { secureList: "auth.Customers\.index", permissions: "list.customers"},
        { secureList: "auth.Customers\.show", permissions: "show.customers"},
        { secureList: "auth.Customers\..*", permissions: "manage.customers"},
        // ....more
        //searchitems
        { secureList: "SearchItems\..*", permissions: "view.searchItems"},
        //BLOCK ALL RULE
        { secureList: ".*", permissions: "UNKNOWN_SO_ALWAYS_FAIL", whitelist=whiteRules.toList() }
    ];
    return aRules;
}

In our case I defined an array of whitelisted items. It is an array because it is a bit easier to handle compared to a list. In my rules array I defined several items to list, show and manage users, each with their own permissions. Please note: as soon as a secureList matches you will never see the rest of your Rules, so the most specific rules should always come first. I have a last rule with non-existing permissions to make sure nothing will pass, unless it is in an explicit whitelist.

Logging in

So now we have everything in place, we only have to make sure we can login. We created an Apikeys handler which has a create method, something like this:

function create( event, rc, prc ){
  event.paramValue("userName","");	
  event.paramValue("password","");	
  var myToken = jwtAuth().attempt( rc.username, rc.password );
  prc.response
    .setStatusCode( STATUS.CREATED)
    .setData({ 'apiKey' = myToken })
//  logAuditMessage(rc.userName,"login",{});
}

As you can see this method is quite simple. It doesn’t even have conditional handling for failed logins. That’s because my jwtAuth().attempt() method will throw. an exception when credentials are not valid. This exception is handled by our REST base handler and will return a nice and clean 401 Not authenticated response. There are many more methods for jwtAuth() which can be found in the documentation.

So this post sums up what you have to do to enable JWT security. Although you have to configure quite some steps, most of them are quite simple. In future posts I will try to create a better permission system and stronger password security.

2 Comments

  1. Cam Penner

    Thanks for the excellent walkthrough! I’m going to have to try this on a project. I was beating my head against a brick wall trying to implement something like this. Do you happen to know if this works without sessions? My big problem was that somewhere in the chain of authentication the AuthenticationService in cbauth was called, and that relies on a Session variable to implement isLoggedIn(). We were hoping to ditch sessions and just use JWT. Our goal was to have the cbAuth AuthenticationsService use the presence of a valid JWT to respond to isLoggedIn() – but we stuck in a bit of a chicken and egg scenario between JWT needing to use Authentication to validate the token and Authentication needing to use JWT validation to authenticate the user.

    • Wil

      The cbauth service is using sessions by default. But that doesn’t mean you have to use it. Cbauth has options to change your sessionstorage, see here https://cbauth.ortusbooks.com/installation-and-usage.
      You could use a cache storage or even RequestStorage@cbstorages for the sessionstorage setting. (I think the name is confusing, it is just where it stores the ID of the user).
      The reason the authentication still works with a short-lived storage such as RequestStorage is that the jwtService has it’s own authenticate method.
      The JWT authenticate method does this:

      • decode your token
      • retrieve user id from decoded token
      • retrieve the user object and login to the authenticationService

      and it does this for every request. So no need for cf sessions.
      I would just give it a try with RequestStorage for the cbauth session storage. I didn’t try it yet, so just let me know if it doesn’t work. JWT should be able to work without sessions.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 ShiftInsert.nl

Theme by Anders NorenUp ↑