This article is part of the Authress Academy and discusses the different ways to invalidate a user's access and revoke their tokens.
It discusses solutions for:
- Denylists
- OAuth JWT token expiry
- API Gateways
- Refresh Tokens
Background
To secure communication between different systems or services that you have, an access token is sent between the user, client, or machine to your service API. Usually, this is some sort of JWT access token.
In the case of Authress and other providers, a JWT token is generated when the user logs in, that token is available for them to gain access to their resources and prove their identity to your services.
When that token expires, they must then fetch a new token. (Fetching new tokens won't be discussed in this article, the KB has additional articles on Authress Authentication.)
However, there are cases where we want to terminate access as soon as possible--User role changes, permission revocation, or token exposures--all may facilitate the need to terminate access.
OAuth JWT access tokens + Scopes
Many authentication solutions and identity providers generate JWT access tokens and provide these as a way to authenticate users to your applications. They are generated for the user in the UI, and are sent to your services via the Authorization
header. Generated JWTs are usually created through a standardized OAuth exchange, the details of which aren't immediately relevant here, but there are a number of other Authress KB articles that discuss the topic. For the rest of the article, we'll assume that you know what a JWT access token and how to get one. (If you are unsure, please don't hesitate to join our Community and ask!)
In the case of authentication solutions that do not offer any sort of authorization or access control, they may attempt to store a user role (or roles) in the JWT in the scope
JWT property claim.
OAuth JWT Scopes
The OAuth JWT scope claim is a property which contains a list of permissions and roles that the user has approved to be passed to the JWT. However, since the User gets their own JWT, this may seem unnecessary complex. The standard OAuth use case is that the requestor of the JWT is not the same as the User themselves. That is, your user is actually approving the generation of the JWT by your UI. And then your service will validate the JWT. Scopes here are a way to restrict what permissions the user is passing to the token. When this JWT is used, the scopes
provide a way to filter the resources that are actually allowed to be accessed.
Example:
- The user has access to all their own resources of the types: documents, videos, photos
- A UI could request the scopes
documents
andphotos
- The user approves the UI requests for those scopes
{
"iss": "https://login.authress.io",
"scope": "documents photos",
"sub": "user_id",
"iat": 1685021390,
"exp": 1685107790,
"scope": "openid profile email",
"aud": [
"https://api.authress.io"
]
}
The generated JWT would only contain the documents
and the photos
scopes and not the videos
one. That means of all the resources that the User has access to, the JWT only has access to user's documents and photos.
You'll notice that scopes are not the same as permissions the user has. The user has permissions to only some resources, but the scopes have even more limited access. Scopes constrain access based on what the user already has permissions to. The optimal use of OAuth JWT Scopes are used when you don't control both the UI and the Service, and there are two reasons for that:
Most Authentication providers allow the User to specify any and all scopes that they want in the token. This means if you are using the Scopes to control access to resources that the user can interact with, you have a vulnerability. You are letting your users control what they have access to. Scopes must not be used for access control for the User.
You'll need some to capture which permissions the user actually gave to another service. If a third party is accessing your resources or you are accessing a third party's resources, the
scopes
granted to the client application, control that access. This is a different list of permissions because it is a different entity performing the API request.
Issues with scopes
Since scopes are embedded into the JWT token, as long as the JWT token is valid (between the nbf
and exp
times) then the holder of that token has access to the user's resources that are granted via the scopes in the token. This means that there is no way to block requests in a situation where you want to invalidate the access that the token grants.
This is the OAuth spec, and while it seems like a missing part of the specification, if we consider the difference between the access granted to the user and the scopes granted to the third party on behalf of the user, it makes sense that in most cases this doesn't matter. Invalidating a JWT wouldn't make sense.
This also makes sense when we consider what these JWT tokens are. A JWT access token is a representation of a user identity. The Authentication Service generates the token to represent the user's identity, and then the user, client, or third party presents that token to prove who they are. However, it doesn't really say anything about what they can access or why. Further, JWTs don't stop representing the user just because their access changes.
As discussed in Academy for access control strategies and token attenuation:
The most common mistake is putting the roles or permissions into the JWT.
When the permissions are in the JWT (using the scope
or a custom claim), the permissions have become coupled to the identity of the user. These are separate things, and deserve separate treatment, and we'll go into some of the further issues and resolutions below.
Revoking a JWT access token
Now that we know that the permissions coupling to the JWT access token creates some problems, we can discuss what we can do about it.
0. Do nothing approach
The default and simple approach is to do nothing. For most implementations and products, this may be the best answer, however if you are in Healthcare, Banking, or another highly regulated government industry, this is not going to be the right approach, and skipping this step is recommend.
When the JWT expires the user will lose access to the scopes and permissions that were specified in the token. This may seem bad from a security standpoint, however in most cases, this is exactly the flow that makes the most sense. Most cases don't need to be concerned with malicious attackers using still valid JWTs. When the user signs out of the UI, discard the JWT. Once it leaves memory it should no longer be a problem. If this were a problem that the hypothetically saved JWT could still be used, we can question How? If the token is available to be used after log out, and we are concerned that it will be, how did that come to happen?
One common answer is that the user is using a shared machine. However on shared machines, user log out is not reliable, and shared machines are not truth-worthy. Simply logging out, even if the token were revoked will not prevent vulnerabilities from existing to utilize the token and impersonate the user. It is an illusion that there is a solution to this problem other than terminating the OS of the machine (which still might not be sufficient.)
1. Limiting the access token lifetime
Going further would be to ensure that the lifetime of the token is as limited as possible. With authentication session management, we can generate an additional token for the user when the current one expires. So instead of 24 hours, 4 hours, or even 1 hour, if the token expiry is in 5 minutes, then every 5 minutes the user will get a new token. Even if the user doesn't log out, that token is going to expiry in the next 5 minutes, almost completely reducing the feasibility of an attack using an exfiltrated token.
Using Authress or another centralized authentication identity provider, you might be able to configure the token lifetime to automatically expire.
2. Use a shared Denylist
In a platform where you have many services, creating an endpoint on one service that allows the user to logout thereby storing that timestamp, isn't sufficient to ensure that the token with the scopes doesn't get used again. This leads to the need to have a unified solution for token management. We already know we need a unified solution for authentication, but this adds an additional layer of complexity. One common solution is the creation of an API gateway. An API Gateway is a reverse-proxy for all the services in your platform.
The API Gateway receives every request, verifies that the token is valid based on your identity provider and internal token cache, and then forwards the request or denies it. Additionally, it can offer an endpoint that allows your services to revoke a still valid identity token.
The drawback with this is that it requires this additional piece of technology on top of your existing Authentication IdP, and it also introduces a requirement that your user logout now needs to call to your exposed API Gateway in order to invalidate these tokens. There are some solutions that make storing this data in a database easier, but it is still something extra you'll need to add to your API Gateway.
Distributed Cache Alternative:
If you aren't interested in setting up an API Gateway, which often is infeasible in a multi-region deployment or in a highly distributed system, then alternatively a distributed shared caching solution can work. The Authress recommendation is to avoid having a shared cache wherever possible, since it increases the cost of maintenance and often serves as a single point of failure that wasn't designed to handle fault tolerance at the scale that IdPs are designed for. (See Authress downtime protections for some of these fault tolerant protections.)
3. Permission changes
So far we have only discussed the need to invalidate credentials when the user logs out, but what happen when their roles and permissions change? Your API Gateway isn't going to be sufficient to know what to do, unless you add permission and role changes to it as well. At that point you've designed your own AuthZ solution, and it would be better to use an existing IdP as part of your reverse-proxy solution. (And in that case, you might want to check out Building your own AuthZ solution.) And worse, if you invalidate the token in the API Gateway, your users in your UI never know they need to get a new token. The invalidation happened on the service side without the users' knowledge. They'll start getting back 401
errors from the API Gateway. A workaround would be to add handling code to your UI to force a re-login when a 401 is seen as a response from a service. But then this code has to be replicated to every UI you have, and not every service might be behind the API Gateway, that means special handling would also be necessary for invalidations to know which service the user is calling from the UI. Some calls can expect a 401
while others a 401
means "my roles changed". That can cause a poor UX for the user, if they are forced to log out every time their roles are changed.
The best solution here is to decouple the authorization from the token itself. By storing the authorization access control and permissions for the user in a separate service from the user identity handling, when the access changes, then no further updates are necessary.
That's because authorization is checked realtime instead of only being populated during token generation. With JWT scopes, the permissions and roles are cemented into the token, but with an Authorization solution the access is dynamic. When roles change, so does the user's live access. This is a significant improvement, because there is no extra storing of access tokens nor a need to invalidate them one by one.
Additionally, depending on the solution you are using, in this case if we assume Authress Authentication, when the user logs out the user's session may also be terminated on the provider side. Meaning that a solution that offers both Authentication and Authorization and keeps them segregated works best for handling token invalidation.
Concerns
One common concern as we go further down this list is the reliance more and more on a shared centralized system. And most shared systems are not fault tolerant--they weren't build to scale nor built to be reliable. Many open source implementations of token invalidation, caching, and gateways suffer from this lack of reliability. And the more reliable they were designed the more difficult they are to maintain, that's because they pass the burden of maintenance onto the development team that runs the open source solution. It's better to go with high SLA AuthN solution that already supports your needs out of the box.
Rather than having to build this yourself and maintain it, finding the right product that fits your needs is a must. The current auth situation report is available in the KB for a deep dive on the different auth technology pieces.
Here are some hints about how reliable these solution are:
1. Reliability designed in
High SLA services (at least four 9's), are designed with this distributed reliability in mind. It isn't a single point of failure since frequently the service itself has been replicated to multiple regions in multiple datacenters with additional backups. your technology often can be anywhere in the world and your users can be on the opposite side, and both your services and the user can experience very-low latency. This is often achieved with distributed CDNs and edge-node authentication.
2. Selection of the right protocol and technology
When designing token invalidations, a large part of the ability to even execute effectively, requires picking technologies that are by default high performance. Some providers offer the EdDSA token signature standard which is faster and produces smaller signatures. Utilizing distributed public keys, enables a hands-off approach to verifying credentials. Using one of these providers that supports public key encryption enables tools like an API Gateway or the services themselves to verify tokens without even making any API calls. The public keys are cacheable for an extended period of time. Enabling fast requests without the single-point of failure.
3. Clear separation of responsibilities
Solutions that recommend storing the access permissions inside the identity JWT immediately get off to a bad start. We've seen from above this encourages coupling in a way which cases issues at the important security edge cases. While it can seem like an optimization for simplicity, it actually just creates problems. There are very few products, services, and applications where JWT scopes for permissions actually is a good fit. Therefore, the selection of a technology that provides the separation between identity and access control is important.
Going further
What about refresh tokens
Refresh tokens
are part of the OAuth standard that exist to enable third party services to impersonate users even when the user's JWT access token has expired. That has nothing to do with token invalidation and don't help us at all here in our use case.
Revocation comes up often with OAuth, because documentation sources often point to Refresh tokens as a solution. That's because in the OAuth spec, Refresh tokens can actually be revoked and solutions, recommendations, and implementations get stuck on this part. Refresh tokens are long lived, and revoking them prevents new access tokens from being generated from that Refresh token. But just because the refresh token is revoked, does not mean that the access token is revoked. And in almost all implementations, that is the case, because access tokens are validated on the client side, but the revocation database is on the service side. So attempting to use refresh token revocation to block access token usage would require every API request to unnecessarily call out to the revocation database to verify the token. This converts the standard distributed offline public key process for token verification to be online, centralized, and slow.
For help understanding this article or how you can implement a solution like this one in your services, feel free to reach out to the Authress development team or follow along in the Authress documentation.
Top comments (2)
I'm having problems parsing this statement:
IIUC access tokens are validated not client side but server side. However a database access is not required to validate the access token. The validation can be done using a static key that is shared by the token validator and generator. Another way is to have the validator use a public key to validate the token signed using the corresponding private key.
In neither case is the token (as sent in the Authorization HTTP header) verified on the client.
If you invalidate a token on the service side, then the user will still be sitting in the UI with an invalid token. That's because the UI doesn't know that the invalidation happened.
If the UI doesn't know that the token has been invalidated, there is no way that it knows to throw away the invalidated token and force the user to log in again.
Actually doing this:
Won't work for any provider, because the token is still valid according to the validation code because all the code runs on the client side.
Let me know if that doesn't clear it up. If there's still some confusion here, it might be easier to run through a case in the discord server.