No, this is not a typo. This post will tell you how to prevent some headache with JWT iss claims in cbsecurity. It is quite easy to solve, but since I just spent several hours debugging some very nasty JWT authentication problem, I thought it might be worth sharing. Bottom line: if you are using the iss claim in JWT make sure you specify it yourself, so don’t rely on the default (although that might look attractive). Better yet: ALWAYS specify the issuer claim, even if you think you are not using it. Only read the rest of this post if you really want to know why.

So what is this iss claim in JWT? Let me quote from the RFC:

4.1.1.  "iss" (Issuer) Claim

The "iss" (issuer) claim identifies the principal that issued the JWT.  The processing of this claim is generally application specific.
The "iss" value is a case-sensitive string containing a StringOrURI value.  Use of this claim is OPTIONAL.

The iss claim is particularly useful for security. You can specify the URI which issued your JWT. On validation cbsecurity checks if the iss claim in your JWT matches your settings. If not, validation will fail.

We are using the iss claim, so we can distinguish between production, staging and dev environments, so you can’t use a JWT issued bij a dev environment in production. Initially we didn’t specify a issuer value in our cbsecurity settings, because the issuer defaults to the base URL of your application, which would be OK. But when we restarted our DEV environment validation failed with a 401 Not authenticated. Since the same token had been used before with success I didn’t understand. I enabled debugging, did find some evidence that authentication (so not authorization) failed. This made no sense to me, since this token was valid before. When I reinitialized my coldbox framework, suddenly the token was valid!

So after some digging i found the cbsecurity interceptor calls preProcess which finally calls a whole chain of methods:

  • securityInterceptor preProcess -> processRules -> JWTService.ruleValidator
  • JWTService rulevalidator -> validateSecurity – > getPayload -> parseToken -> decode – > jwt-cfml jwt.decode
  • jwt,decode -> verifyClaims.

None of these methods had any debugging and the tricky thing with our setup is that most exception in cbsecurity finally lead to a NotAuthorized or NotAuthenticated exception, which is handled by a Rest Basehandler. In the process a lot of this error info is lost, I only get a not so informative NotAuthenticated message. But finally I arrived at the decode method of the jwt-cfml JWT library, which calls a verifyClaims method

try {
  return variables.jwt.decode(
    token      = arguments.token,
    key        = variables.settings.jwt.secretKey,
    algorithms = variables.settings.jwt.algorithm,
    claims     = { "iss" : variables.settings.jwt.issuer }			 
  );
//....

The verifyClaims method throws an exception on my iss claim, while it only receives some static settings information variables.settings.jwt.issuer. And after framework reinitialization the verifyClaims method is not throwing exceptions at all. So some further logging on my iss claim reveals it is changing the claim after a reinit!

Initially my iss claim was
http://somesite.mysite.nl:8443/
but after reloading the framework it suddenly changed to
http://somesite.mysite.nl:8080/.
These are the secure and insecure version versions of my dev environment. So how can these settings change if I don’t touch it??

The search continued.. The JWTService creates some default settings which looks like this for the issuer claim:

// Check if issuer is set, if not, default to the home page URI
if ( !len( variables.settings.jwt.issuer ) ) {
  variables.settings.jwt.issuer = requestService.getContext().buildLink( "" );
}

So apperently this buildLink method returns the secure version of my app at port 8443 on startup, but on a reinit it generates the unsecure version at port 8080. My conclusion is clear. If I can’t rely on this buildLink method generating something consistent I should not use it. In this case it means I should create my own issuer setting. This is quite trivial to do. In the cbsecurity settings I specify:

cbSecurity : {
  "authenticationService" : "authenticationService@cbauth",
//...
  "jwt                    : {
    "issuer"	            : "https://backend.mysite.nl/",
//.. or you can do
    "issuer"              : getSystemSetting( "JWT_ISSUER", "" )

The second example puts my JWT_ISSUER value in an environment variable wich is different on each system. If I keep the value just in my coldbox.cfg I can specify different settings for my dev environment like this:

function development(){
  //change to higher expiration for dev only
  moduleSettings["cbSecurity"]["jwt"]["expiration"]= 31*24*60;
  // hardcode different issuer for dev
  moduleSettings["cbSecurity"]["jwt"]["issuer"]= "https://dev.mysite.nl:8443/";
//...

So, that was fun. Digging through many many functions in cbsecurity and jwt-cfml just to find out buildlink() is not returning the same value all the time. So main advice again: always specify your issuer setting. The tricky thing: even if you are NOT using issuer at all, this default setting is still used for validation, which leads to unexpected results. I think cbvalidation should skip checking this iss if it is not a required claim, and probably buildlink should always return the same base. But I will investigate that later. But if you are using both secure and insecure versions of your app, a proxy or multiple urls which lead to the same site it is still wiser to hardcode this issuer in your coldbox settings or environments.

I think I’ll put some warning in the documentation 🙂