loopbackio / loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.
https://loopback.io
Other
4.93k stars 1.06k forks source link

feat(authentication): refreshing/replacing the token #4573

Closed derdeka closed 3 years ago

derdeka commented 4 years ago

Suggestion

TTL of AccessToken (JWT, LB3 legacy token or any other token system) should be extendable or token should be replaced after some time of beeing used. AFAIK the @loopback/authentication component does not support this yet.

Use Cases

Cross-Posting @sformisano 's comment from https://github.com/strongloop/loopback-next/issues/3673#issuecomment-560954366 as discussion point:

Most JWT based auth systems do actually carry a refresh mechanism for the access token, either through a secondary refresh token that lasts longer than the access token or with a long-living access token that also works as a refresh token.

Based on how this seems to work at the moment in LB4, if I want to set a low TTL for the access token, which would be good for security, once the token expires, I would have to login again, even if I was using the app right when the access token expired.

I'm sure users having to log in every 10 minutes is not what the LB team is shooting for so why don't we review a few options to avoid that:

1) The simplest approach: very long-lived access token with no refresh mechanism. This means creating access tokens with TTL set to weeks or months. As a consequence, the problem described above is vastly mitigated, but still not resolved. The user will still, at some point, be logged out, even if it uses the app every single day. The user could even be logged out while it is using the app because when the token does expire, the only way to get a new one is to log in again.

This solution is also not great from a security standpoint: by definition, an access token is a token that provides access to resources, and in a stateless authentication environment a token of this kind that lasts for months can be problematic. There are a number of ways to mitigate this type of issue as well, but they are not very efficient.

One example would be having an access tokens blacklist. Every time there's a request, the token would be verified against the blacklist. Running this type of check for every requested is not the most efficient way to solve this problem.

A user-level ban mechanism would have the same exact efficiency limitation.

As far as I can see, this is the only solution that seems to be available out of the box within @loopback/authentication, which is why I started looking for this kind of discussion.

2) A better approach: long-lived access token with refresh mechanism. This solution would have a single access token with a considerable TTL, but not as long as for solution (1), perhaps a few days. The improvement over solution (1) is that the API would be responsible for refreshing the access token and sending them back to the client regularly. It would work like this:

Every time there's a request, access token TTL is checked by the API.

If the access token TTL falls below a certain threshold (e.g. 50% of original TTL), a new access token would be created and automatically sent to the client.

The client would notice a new access token has been sent back, and it would replace the old one with the new one.

This resolves the involuntary mandatory logout issue from solution (1), and it also improves scalability and security: the token no longer needs to last for very long (security), and if there needs to be any kind of ban/moderation feature available to intervene against malicious users, it can be implemented within the refresh mechanism rather than in every single request (efficiency) made to the API.

Still, the refresh procedure would only happen when the access token is beyond a certain lifetime threshold, which would still mean that it would take a while to be able to ban someone.

This brings us to my favorite solution:

3) The best solution (IMHO): short-lived access token and very long-lived refresh token. Under this paradigm, two tokens are issued at login time: an access token, i.e. the token giving access to resources, with a very short TTL (e.g. 10 minutes), and a refresh token, i.e. a token whose only ability is that of requesting a new access token. The auth flow would work like this:

The token is implicitly verified to authenticate the user. If the access token expired, we proceed to the next step.

The refresh token is verified. As this token lasts for very long, chances are it is still valid, and if it is, we can use this token's authority to generate a new access token that will once again last very little, e.g. 10 minutes.

We return the new access token to the client, which will replace the expired access token with a new valid one. While we're at it, we'll also generate a new refresh token and update that one on the app as well, so that we can avoid running into any issues with this one expiring at some point down the road (i.e. the same issue we had in solution 1).

This fixes all the problems reviewed above:

The token providing access to the resources in the API is extremely short-lived = better security

The refresh token has a high TTL, but it does not provide access to API resources, it can only be used to ask for a new access token, and when that request is made, we can run all our security checks/procedures. The refresh procedure would be triggered very often because the access token has a short TTL, which means a malicious user is forced through security checks regularly while still not causing a potential performance issue at scale.

This last point may sound trivial if your API is a monolith, but in a microservices environment, this matters a lot: any microservice that is not directly responsible for authentication/authorization should only be asked to verify the access token's signature and TTL. If the token is not expired, and if the signature is valid, then for all intents and purposes you are authenticated as far as those microservices know.

Therefore, if a self-sufficient access token lasts for months, all your microservices will consider a potentially malicious user as logged in, for months, and they literally do not have the ability to intervene in any way.

On the other hand, if the access token lasts only for a few minutes, the malicious user will have to go back to the authentication microservice, which is the one holding all the security checks functionality.

I know this was very long, but I think it's important to start a conversation on how the default authentication library for LB4 should work.

Do you guys think implementing a flow like the one described in solution (3) would be unreasonable?

Is there anything I am missing in the current LB4 authentication flow that renders what I wrote above incorrect?

Like I said earlier I'd love to help, so please let me know what your thoughts are.

Thanks!

Acceptance criteria

TBD - will be filled by the team.

dougal83 commented 4 years ago

Adding token refresh would add a bit of polish. 👍

derdeka commented 4 years ago

@sformisano I would prefer option 3 too.

To make this working i would like to extend the TokenService: https://github.com/strongloop/loopback-next/blob/6eea5e428b145cafb84a998bd53979da8c8fba07/packages/authentication/src/services/token.service.ts#L11-L20

E.g. by adding a refreshToken method:

  /**
   * Renews, refreshes or extends ttl a given token string.
   *
   * @param token A current valid token.
   * @param secret A secret, e.g. refreshtoken to secure the operation.
   * @returns A promise which resolves to a new or refreshed token
   */
  refreshToken(token: string, secret?: string): Promise<string>;

The TokenService needs to:

Anything i missed?

dhmlau commented 4 years ago

@derdeka, since you have a PoC, let me assign this to you. Thanks.

nflaig commented 4 years ago

While we are at it, I would also suggest that the tokens are HttpOnly cookies instead of storing them in localStorage, see here for why this is important. Another advantage of cookies is that the server can set and remove the tokens and clients do not need to know anything about the authentication flow / implementation. The problem with cookies is that there needs to be some sort of csrf protection which could be achieved by introduction an additional csrf-token header that needs to be send by the client or even setting the cookies to sameSite=strict could do the job in modern browsers. Another important point that was not mentioned above is that if the access token is long lived it is impossible to change the access rights of the user which means in a real world application this is not suitable at all.

@derdeka I already developed the authentication flow you described above but in a microservice context. I also think that JWT in general makes more sense in that context, if your application is a monolith I would rather go for session cookies

dougal83 commented 4 years ago

@nflaig 👎 for cookies. LocalStorage is better suited to the task at hand. Cookies break RESTful best practices as it requires the server to store session information.

nflaig commented 4 years ago

@dougal83 I guess you didn't understand my proposal correctly. We are not storing any session information on the server, the only thing that changes is how the JWT is stored on the client which is localStorage vs HttpOnly cookie.

dougal83 commented 4 years ago

@dougal83 I guess you didn't understand my proposal correctly. We are not storing any session information on the server, the only thing that changes is how the JWT is stored on the client which is localStorage vs HttpOnly cookie.

You could be right but I'm just not convinced atm. I'll read more about it and come back if I change my mind.

Another important point that was not mentioned above is that if the access token is long lived it is impossible to change the access rights of the user which means in a real world application this is not suitable at all.

Furthermore, I don't really see the above statement being a concern with regard to inclusion of the refresh token that this PR proposes. The short lived access token contains the permissions/scopes. The longer lived refresh token simply requests/refreshes the access token and at the point of reissue we can check if access has been revoked or altered.

nflaig commented 4 years ago

You could be right but I'm just not convinced atm. I'll read more about it and come back if I change my mind.

There are a lot of great resources with information on web security best practices for example OWASP cheat sheets. Also if you take a look how websites store sensitive information you will not find a single one that uses local storage.

Furthermore, I don't really see the above statement being a concern with regard to inclusion of the refresh token that this PR proposes. The short lived access token contains the permissions/scopes. The longer lived refresh token simply requests/refreshes the access token and at the point of reissue we can check if access has been revoked or altered.

The PR listed 3 different options. This was just another argument against option 1 and why it is not a suitable approach.

dougal83 commented 4 years ago

You're right that LocalStorage shouldn't be used to store sensitive information. However the OWASP Docs don't highlight a particular issue. The JWT token, is fine in LocalStorage as it is really just a verifiable claim and not the actual credentials for instance.

This was just another argument against option 1 and why it is not a suitable approach.

Fair enough, I boiled down the post in my head to option 3 in agreement with @derdeka.

nflaig commented 4 years ago

They are highlighting some issues like XSS in the chapter about local storage. I think the highest risk might be a malicious npm module you are using in your UI code which just gets all items in local storage and sends those to the attacker. I can't really tell how likely it is that such an attack happens but I guess when it comes to security one should try to follow best practices to avoid potential exploits.

derdeka commented 4 years ago

@nflaig I don't make any asumptions how the tokens are stored (in cookies or in localstorage). I think this is in the responsibility of the frontend and is out of scope of my POC. But i'm open to discussions about best practices and feedback how other developers handle this. In my case i have a angular frontend which should periodically refresh the accessToken to avoid interuption of user interaction.

nflaig commented 4 years ago

@derdeka yes it probably makes sense to discuss this topic in another issue.

I think this is in the responsibility of the frontend and is out of scope of my POC

This can not be decided by the frontend since the backend sets the cookies in the response. In case the cookies are Http only it is even impossible for the UI javascript code to interact with the cookies at all. But I guess the first step is anyways to do the PoC implementation with refresh tokens and the next step would be security considerations. In my opinion even though this is just a demo application it should still showcase best practices.

derdeka commented 4 years ago

This can not be decided by the frontend since the backend sets the cookies in the response.

@nflaig I don't see any cookies in the login process set by loopback-next or loopback4-example-shopping. Which application are you referencing?

nflaig commented 4 years ago

@derdeka loopback4-example-shopping does not use cookies but if it would it has to be decided in the backend. Of course if the token is sent back in the request body for example you can decide in the frontend where you want to store it. Usually the backend would use the set-cookie header in case the tokens are intended to be stored as cookies.

achrinza commented 4 years ago

I agree that tokens should be stored as a HTTPOnly cookie to prevent XSS.

However if I'm not mistaken, verifyToken() and generateToken() do not assume where the token is stored (and neither will refreshToken()). Furthermore, the authentication package only stores the interfaces and do not contain any implementation of the aforementioned functions.

This seems to be more of an issue on how loopback4-example-shopping implements these interfaces thereby out-of-scope of this issue.

dougal83 commented 4 years ago

This seems to be more of an issue on how loopback4-example-shopping implements these interfaces thereby out-of-scope of this issue.

Agreed. Client side implementation is out of scope of PR.

achrinza commented 4 years ago

Just to clarify; are we simply adding a refreshToken function to the interface?

If so, we should land this before the loopback4-shopping-example PoC.

MattRiddell commented 4 years ago

Is there any documentation about how to make this work with the example?

raymondfeng commented 4 years ago

See https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt#endpoints-with-refresh-token

dhmlau commented 3 years ago

Closing as done.