Skip to content

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

@derdeka

Description

@derdeka

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 #3673 (comment) 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.

  1. 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:

  1. 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions