Why caching access token is consider bad in oauth2

2019-02-03 12:29发布

问题:

I am following this article for revoking user access :

http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/

Now consider after validating user I have issued an accesstoken with 30 minutes life span as shown in above article and with refresh token as 1 day but what if admin delete that user in 10 minutes with 20 minutes still left so now in this case I need to revoke access that user.

In order to do this I need to remove that user entry from refresh token table to disallow further access token request but as accesstoken expire time is still having 20 minutes so user would be able to access protected resource which is completely wrong.

So I was thinking to implement caching mechanism to cache the access token on server and also save in database. So when that user is revoked I can simply remove that user entry from cache and database to stop that user access from accessing protected resource.

But this 2 below answers are saying this is not how oauth2 was designed :

Revoke access token of OAuthBearerAuthentication

OAuth2 - unnecessary complexity with refresh token

So my question is :

1) Why caching access token is not considered better than refresh token mechanism and also a bad approach?

My second question is based on this below answer given by @Hans Z. in which he is saying that :

This necessarily would involve the Resource Server (RS) consulting the Authorization Server (AS) which is a huge overhead.

2) In case of revoking access for a user why does RS would consult AS because AS is just for authenticating user and generating access token as per this Article?

3) In the article there are only 2 projects :

  • Authentication.api - Authenticating user and generating access token
  • Resource server - validating accesstoken with the help of [Authorize] attribute

    In above case which is the authorisation server then?

Update : I have decided to use refresh token to revoke user access in case user is deleted and also when user logout i will refresh token from refresh token table because of your requirement that we want to logout user immediately as soon as user clicks on logout.

But here the problem is i have 250 roles associated with user so if i put roles in accesstoken then size of accesstoken will be so huge and we cannot pass such huge accesstoken from header but i cannot query roles for validating user access for endpoints each time that endpoint is called.

So this is 1 another problem which i am facing.

回答1:

There seems to be 2 different questions here: about access token and about big list of roles.

Access Token

OAuth2 was designed to be able to handle high load and this requires some trade-offs. Particularly this is the reason why OAuth2 explicitly separates "Resource Server" and "Authorization Server" roles on the one hand, and "access token" and "refresh token" on the other hand. If for each request you have to check user authorization, it means that your Authorization Server should be able to handle all requests in your system. For high load systems this is not feasible.

OAuth2 allows you to make the following trade-off between performance and security: Authorization Server generates an Access Token that can be verified by the Resource Server without accessing Authorization Server (either at all or at least not more than once for a life of Authorization Server). This is effectively caching of the authorization information. So in this way you can drastically reduce load on your Authorization Server. The drawback is again the same as always with caching: authorization information might get stall. By varying access token life time you can tune performance vs. security balance.

This approach also might help if you do micro-services architecture where each service has own storage and don't access each other's.

Still if you don't have much load and you only have single Resource Server rather than tons of different services implemented using different technologies, there is nothing that prohibits you from actually doing full-blown validation on every request. I.e. yes, you may store Access Token in the DB, verify it on every access to the Resource Server and remove all Access Tokens when user is deleted, etc. But as @Evk noticed, if this is your scenario - OAuth2 is an overshoot for you.

Big list of roles

AFAIU OAuth2 doesn't provide an explicit feature for user roles. There is "Scopes" feature that might be also used for roles and its typical implementation it will produce too long string for 250 roles. Still OAuth2 doesn't explicitly specify any particular format for access token, so you can create a custom token that will hold roles information as a bit mask. Using base-64 encoding you can get 6 roles into a single character (64 = 2^6). So 250-300 roles will be manageable 40-50 chars.

JWT

Since you'll probably need some custom token anyway, you might be interested in the JSON Web Tokens aka JWT. In short JWT lets you specify custom additional payload (Private claims) and put your roles bitmask there.

You may actually use JWT alone without whole OAuth2 stuff if you don't really need any of the OAuth2 advanced features (such as scopes). Although JWT-tokens are supposed to be validated just by theis contents only, you may still store them in your local DB and do additional validation against the DB (as you were going to do with access refresh token).


Update Dec 1, 2017

If you want to use OWIN OAuth infrastructure, you can customize token format providing custom formatter via AccessTokenFormat in OAuthBearerAuthenticationOptions and OAuthAuthorizationServerOptions. You may also override RefreshTokenFormat.

Here is a sketch that shows how you can "compress" roles claims into a single bitmask:

  1. Define your CustomRoles enumeration that list all roles you have
[Flags]
public enum CustomRoles
{
    Role1,
    Role2,
    Role3,

    MaxRole // fake, for convenience
}
  1. Create EncodeRoles and DecodeRoles methods to convert between IEnumerable<string> format for roles and base64-encoded bit mask based on CustomRoles defined above such as:
    public static string EncodeRoles(IEnumerable<string> roles)
    {
        byte[] bitMask = new byte[(int)CustomRoles.MaxRole];
        foreach (var role in roles)
        {
            CustomRoles roleIndex = (CustomRoles)Enum.Parse(typeof(CustomRoles), role);
            var byteIndex = ((int)roleIndex) / 8;
            var bitIndex = ((int)roleIndex) % 8;
            bitMask[byteIndex] |= (byte)(1 << bitIndex);
        }
        return Convert.ToBase64String(bitMask);
    }

    public static IEnumerable<string> DecodeRoles(string encoded)
    {
        byte[] bitMask = Convert.FromBase64String(encoded);

        var values = Enum.GetValues(typeof(CustomRoles)).Cast<CustomRoles>().Where(r => r != CustomRoles.MaxRole);

        var roles = new List<string>();
        foreach (var roleIndex in values)
        {
            var byteIndex = ((int)roleIndex) / 8;
            var bitIndex = ((int)roleIndex) % 8;
            if ((byteIndex < bitMask.Length) && (0 != (bitMask[byteIndex] & (1 << bitIndex))))
            {
                roles.Add(Enum.GetName(typeof(CustomRoles), roleIndex));
            }
        }

        return roles;
    }
  1. Use those methods in a custom implementation of SecureDataFormat<AuthenticationTicket>. For simplicity in this sketch I delegate most of work to standard OWIN components and just implement my CustomTicketSerializer that creates another AuthenticationTicket and uses standard DataSerializers.Ticket. This is obviously not the most efficient way but it shows what you could do:
public class CustomTicketSerializer : IDataSerializer<AuthenticationTicket>
{

    public const string RoleBitMaskType = "RoleBitMask";
    private readonly IDataSerializer<AuthenticationTicket> _standardSerializers = DataSerializers.Ticket;

    public static SecureDataFormat<AuthenticationTicket> CreateCustomTicketFormat(IAppBuilder app)
    {
        var tokenProtector = app.CreateDataProtector(typeof(OAuthAuthorizationServerMiddleware).Namespace, "Access_Token", "v1");
        var customTokenFormat = new SecureDataFormat<AuthenticationTicket>(new CustomTicketSerializer(), tokenProtector, TextEncodings.Base64Url);
        return customTokenFormat;
    }

    public byte[] Serialize(AuthenticationTicket ticket)
    {
        var identity = ticket.Identity;
        var otherClaims = identity.Claims.Where(c => c.Type != identity.RoleClaimType);
        var roleClaims = identity.Claims.Where(c => c.Type == identity.RoleClaimType);
        var encodedRoleClaim = new Claim(RoleBitMaskType, EncodeRoles(roleClaims.Select(rc => rc.Value)));
        var modifiedClaims = otherClaims.Concat(new Claim[] { encodedRoleClaim });
        ClaimsIdentity modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
        var modifiedTicket = new AuthenticationTicket(modifiedIdentity, ticket.Properties);
        return _standardSerializers.Serialize(modifiedTicket);
    }

    public AuthenticationTicket Deserialize(byte[] data)
    {
        var ticket = _standardSerializers.Deserialize(data);
        var identity = ticket.Identity;
        var otherClaims = identity.Claims.Where(c => c.Type != RoleBitMaskType);
        var encodedRoleClaim = identity.Claims.SingleOrDefault(c => c.Type == RoleBitMaskType);
        if (encodedRoleClaim == null)
            return ticket;

        var roleClaims = DecodeRoles(encodedRoleClaim.Value).Select(r => new Claim(identity.RoleClaimType, r));
        var modifiedClaims = otherClaims.Concat(roleClaims);
        var modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
        return new AuthenticationTicket(modifiedIdentity, ticket.Properties);
    }
}
  1. In your Startup.cs configure OWIN to use your custom format such as:
var customTicketFormat = CustomTicketSerializer.CreateCustomTicketFormat(app);
OAuthBearerOptions.AccessTokenFormat = customTicketFormat;
OAuthServerOptions.AccessTokenFormat = customTicketFormat;
  1. In your OAuthAuthorizationServerProvider add ClaimTypes.Role to the ClaimsIdentity for each role assigned to the user.

  2. In your controller use standard AuthorizeAttribute such as

    [Authorize(Roles = "Role1")]
    [Route("")]
    public IHttpActionResult Get()
    

For convenience and some safety you may subclass AuthorizeAttribute class to accept CustomRoles enum instead of string as role configuration.



回答2:

I hope I got your questions right and can provide some answers:

1) You can cash it if you developed your AS to require validating it each time the user login.

2) I think @Hans Z. means revoking user by the AS. When RS revoke the user it doesn't change the fact that they are still the ones identified by AS. But when AS revoke the user, it prevent their use of their identity.

3) The article probably assumes that authorization is done by the RS, AS is only responsible to tell you who is the users, and the RS is to decide authorization based on that.



回答3:

Main advantage of refresh token approach is to reduce number to database queries, the access token has the claims and signed so the token can be trusted without querying the database.

Caching access token will work but then you will have to query the cache on each request.

It's a trade off you have to choose between, n minutes of delay in access permission changes vs. number of queries to check access token validity

With added complexity you can almost achieve both in that case you will have to store the cache in server RAM, and stored only revoked tokens to keep the list small. The complexity comes when you have multiple instances of servers you will have to keep this cache of revoked tokens in sync between your RSs and AS.

Basically when the access token is revoked the AS will have to inform all the RSs to add that access token to revoked token cache.

Whenever there is a resource request RS will check if the token is revoked or not, if not revoked RS servers the resource. This way the overhead is there on each request but it is highly reduced as the cache is in the memory and number of revoked tokens will be very less compared to number of valid tokens.