You are not logged in.
Following the advice of ab to not expose a TSQLModel in a web service intended to publish a web api to a javascript application, I created a simple method based class to handle user authentication, using an abstract username/password check and jwt afterwards. This class is intended to be used as a base class to descent a front desk method based service to implement the actual web api. You can find the source code below:
https://gist.github.com/dadim/4443f9062 … 24a70d2077
The class TFronDeskMethodService descents from TSQLRestServerFullMemory. It has two published methods, Login and Logout. The first expects a POST method request with a json with two properties, username and password (plain). Login uses an abstract method, BackendLogin, which checks the user's credentials against a back end service (e.g. another mORMot service, an external provider etc.). If it is successful, computes and sends to the web client a jwt token as the last item of the results array. The secret of all computed tokens is a random 32 bytes long RawBytesString, created in every instance of this class.
To prevent sidejacking, I followed OWASP's recommendation to include in the token (as subject), a random string for each authenticated user. This random string is also sent to the user as a hardened cookie. The class uses also a TSynDictionary to store blacklisted token IDs, whose entries timeout after the elapsing of fJWTMinutesValid (by default 15 min).
Every other api method has to check the request in a similar way as the logout method does, e.g. by a combination of AuthenticationCheck, blacklist dictionary lookup and subject check. The token is renewed by CheckSubject method if it is called within the last 33% of the fJWTMinutesValid time.
You are welcomed to use, modify, distribute it or at least post comments or suggestions.
Last edited by damiand (2021-04-01 17:42:45)
Offline
I have doubts about some of the pattern used.
1) OWASP's recommendation makes no sense to me.
You don't have a JWT any more, but a "classical" session token.
Some arguments in their article are dubious for sure - like using a hash of some random value... they reinvent JWT signature with no real benefit.
If you have a token compromission, then a) either your expiration time is too high; b) your TLS link is broken; c) your client security is broken. In such cases, the JWT is the least of your worries....
2) Server-side renewal is not a good idea. It voids most of the JWT expiration benefits.
Just let the client check for the JWT expiration, and renew it some time before expiration.
On the client side, you may safely store the credentials (user/password) in memory using our CryptDataForCurrentUser() function, to avoid prompting the end-user.
3) You don't need to maintain any blacklist if you properly handle authorization. Maintaining some lists is also voiding some of the JWT benefits.
We usually implement two patterns:
a) The JWT secret is random, and created at service startup. If we suspect some compromission, then we restart the service (or just notify the service to create a new secret). The clients will need to re-authenticate, but if would be silent and painless if you followed my point 2) above.
b) There is a SessionID session in mORMot. Just store the session ID within the JWT, and compare it with the token. When the client closes its session, the SessionID will be rejected. The mORMot in-memory sessions are a white list of users - so you don't need any black list.
Offline
I think I will implement a standard JWT authentication for mORMot 2, including OAuth and easy JS integration.
In addition to the default authentication.
By reinventing the wheel, it is easy to leave some security traps open.
Offline
You don't have a JWT any more, but a "classical" session token.
I could understand this argument if the token is actually stored in the cookie, but this is not the case here. The hardened cookie is used just to create a path outside javascript scope, so the server can cross check not only the token's signature but also the client's origin (sender). That is I think the reasoning of the OWASP recommendation.
On the client side, you may safely store the credentials (user/password) in memory using our CryptDataForCurrentUser() function, to avoid prompting the end-user.
This is suitable for a Delphi client, but in this case I'm talking about a javascript client (e.g. a single page js application). As far as the service receives valid requests I see no reasons to leave the token expire and prompt the web user to enter again the credentials, moreover since the timeout is kept quite short (15 minutes).
There is a SessionID session in mORMot. Just store the session ID within the JWT, and compare it with the token. When the client closes its session, the SessionID will be rejected.
The web client's session is between the js web app and the front desk in memory service which by default doesn't handle user's authentication (aHandleUserAuthentication is false). So I presume that any SessionID will be between the front desk service and another mORMot service which maintains the user credentials. It doesn't seem very useful to me to pass this SessionID to the web client.
Thanks @ab for your remarks.
Last edited by damiand (2021-04-01 18:58:56)
Offline
If the JWT is not sent as a cookie, then you should use POST and put token as parameter - putting the JWT in the URI is bad practice since it may appear e.g. in the logs.
The easiest way is to put the JWT as a cookie, not tweak the JWT content.
You don't need to keep the expiration short in the real world, if you have good security.
Having the user enter its credentials even every 15 minutes is not a huge problem - most of the sessions won't last more than 15 minutes.
Offline
@ab, I don't put the JWT in the URI. The Login request uses POST method and the token is returned as the last item of the Ctxt.Results array, in the context of the POST response.
if bSuccess then
Ctxt.Results(['User ' + jData.username + ' logged in successfully', token])
else Ctxt.Error('Incorrect username or password', HTTP_UNAUTHORIZED);
At the client's side the token is stored in a global js variable or in the sessionStorage and it is included in the requests' header (as a Bearer).
Concerning the expiration, I think that as long as the client is active (e.g. sends requests to the server), it will be annoying to prompt for a login page. If the client is idle, then it is reasonable ask for a new login, since the token is expired.
Last edited by damiand (2021-04-02 10:12:51)
Offline
I think you missed the point ab made, you don't have to prompt login, just renew the token before it expires. You can track expiration js client side and have client submit a renewal request to the server.
Offline
OK I see. However, I think that the token's renewal in the context of a valid request when it is close to expire is simpler (it acts also as an implicit renewal request), with no need to implement a separate renewal action from the client's side or worse, to put this action inside polling (e.g. like window.setInterval etc.), which is more error-prone and risks mishandling of idle user. The only consequence is a need for a blacklist (also recommended as a good practice by owasp), which is covered fine by a TSynDictonary property.
Offline
Three variables (login + password + expiration timestamp) seems not very difficult to implement on the client side.
What you could do is return the expiration delay when you login, so that the client is able to check its expiration without parsing the JWT.
One benefit is that the server could be stateless.
Offline