Some days ago I was polishing my login procedure for my shiny new JWT cbsecurity. When my users are providing a valid username and password I wanted to update their lastLoginDate property, so I can see from my user list when they used the system for the last time. This doesn’t sound to complicated, so I created this code for my login procedure:
try {
// authenticate yourself
var myToken = jwtAuth().attempt( rc.username, rc.password );
// get the current user and update login date
jwtAuth().getUser().setDateLastLogin(now()).save();
prc.response
.setStatusCode( STATUS.CREATED )
.setData({ 'apiKey' = myToken })
}
catch (InvalidCredentials e) {
prc.response
.setStatusCode( STATUS.NOT_AUTHENTICATED )
.setStatusText( "Invalid username or password" )
.addMessage( "Invalid username or password" );
}
So what’s happening here? I try to get a token by using the jwtAuth().attempt()
method. The method succeeds, so I get a token which I have to return to my user. Because of this success I want to update my user’s lastLogin date. Jwt has some handy method to retrieve the current user, so I calledjwtAuth().getUser()
, update the DateLastLogin
property and save the user. Unfortunately this fails: “General application error: Token not found in authorization header or the custom header or the request collection”. So although I just received a valid token, the system doesn’t know about the current user yet.
Is this a bug? It depends how you look at it. cbSecurity with jwt will assume you can be validated if
- you provide a valid bearer authentication header, ie :
Authorization : Bearer hereComes.your.Jwt
- or you provide a valid other header as defined in
cbsecurity.customAuthHeader
setting. The default is:x-auth-token
- or you provide the token in the requestcollection. It has the same cbsecurity.customAuthHeader name, so if you have a rc[“x-auth-token”] variable (in the default setting) with a valid jwt token you will be fine.
According to this three conditions, I am still not logged in. I have received a token in my code (on line 3) which I returned in the response. That’s all. Most of the time it’s ok, but if you want to act immediately on the user object (or some other jwt related method which assumes the token is there) there’s an easy trick. Just put it in the rc directly after your login attempt like this:
var myToken = jwtAuth().attempt( rc.username, rc.password );
// x-auth-token OR the value as defined in cbsecurity.customAuthHeader
rc["x-auth-token"]= mytoken;
// get the current user and update login date
jwtAuth().getUser().setDateLastLogin(now()).save();
So that was easy! Happy now? Not really. I’ll explain why. It is trivial to add this line to the cbsecurity source, so it will immediately behave as if we just logged in, instead of waiting to a next request. I’ll create a pull request for that, and it is up to others to decide if it is a valid choice. Now I know this I don’t care. I can add this extra line.
But there’s something else which didn’t make me happy, and I found out when trying to debug my issue. When the jwtAuth().getUser()
method was throwing exceptions I tried to make it conditional by using the jwtAuth().isLoggedIn()
method. To my surprise it returned true, even when jwtAuth().getUser()
was not able to return the user. That’s at least confusing. The jwtAuth().isLoggedIn() method is shows quite erratic behaviour. So I created the following code and executed it with several different conditions:
var resultA = jwtService.isLoggedIn();
var decodedToken = jwtService.parseToken();
var resultB = jwtService.isLoggedIn();
I logged in to the system, obtained a token and followed this login request with a second request with the above code in the following scenarios.
scenario | jwtToken | cbauth session Storage | cbsecurity | resultA | resultB |
---|---|---|---|---|---|
1 | none | request | securelist | false | exception |
2 | none | session | securelist | true or false1 | exception |
3 | yes | request or session | securelist | true | true |
5 | yes | request | whitelisted | false | true |
6 | yes | session | whitelisted | true or false1 | true |
As you can see, the results of my isLoggedIn() function is quite different each time
- No token provided, so we are not logged in (see resultA column). If we try to parse the non-existing token we get. an exception, so this behaviour is normal
- In this case, we are logged in although we don’t have a token. This is incorrect, and this just happens because cbauth is storing a userId in a session. But all other token info is not there, so our second step fails.
- If we provide a token and our event is secured by cbsecurity, login information is correct
- if we provide a token, store cbauth userId in a requestStorage our first call to IsloggedIn is false. Only after parsing our token we are logged in in ResultB.
- this scenario is quite simular to 4. Only in this case IsLoggedIn is true most of the time, because it is depending on session storage.
I spent quite some time searching for an explanation for this results. IsLoggedIn is just a shortcut to the cbauth isLoggedIn function. I think this has to be changed. JWT is used in APIs most of the time where session storage is not very desirable. So isLoggedIn should check for a valid token and login to cbauth based on this token, and only return true if both conditions are true.
Other results can be explained by the fact that jwtAuth().parseToken() is not only parsing the token, but also calling the cbauth login, which is a good thing: If you have a valid token you should be logged in. If an event is secured cbsecurity will parse the token and you will be OK. If your event is on a whitelist however, there is no token parsing, even though there is a valid token, so isLoggedIn will return false.
So what can we conclude from this? Let me start by saying I still like the jwt handling in cbsecurity a lot. All encoding and decoding is fine, there is multiple mechanisms for token invalidation, there’s automatic logins on the presence of a token, and we don’t need session storage anymore. So a lot of the hard work already has been done. There’s just a few things to remember:
- put your token in your rc immediately after your jwtAuth.attempt() call if you have more code in the same handler which depends on jwtAuth()
- Don’t rely on the isLoggedIn function at the moment, unless you are sure jwtAuth().parseToken() has been called. I will create a pull request for this one.
- When using JWT you should not rely on cfml sessions. Since cbsecurity JWT authentication is calling the cbauth module, make sure you modify the cbauth settings so it will NOT use cfml sessions by changing the cbauth module setting for sessionStorage, e.g:
modulesettings.cbauth.sessionStorage = "RequestStorage@cbstorages"
It may sound counter intuitive, but the only thing this setting does is deciding where your userId will be stored. Naming it userIdStorage instead of sessionStorage would be more appropriate. Since you send your userId in a JWT on every request you don’t have to store it in a cfml session.
Leave a Reply