为什么在oauth2中认为缓存访问令牌是错误的? [英] Why caching access token is consider bad in oauth2?

查看:90
本文介绍了为什么在oauth2中认为缓存访问令牌是错误的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在关注这篇文章,以撤消用户访问权限:

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

现在考虑在验证用户之后,我已经发出了寿命为30分钟的访问令牌,如上面的文章中所示,刷新令牌为1天,但是如果管理员在10分钟内删除该用户(还有20分钟)又该怎么办,那么现在在这种情况下我需要撤消对该用户的访问权限.

为此,我需要从刷新令牌表中删除该用户条目以禁止进一步的访问令牌请求,但是由于accesstoken到期时间仍然有20分钟,因此用户将能够访问受保护的资源,这是完全错误的. /p>

因此,我正在考虑实施缓存机制,以将访问令牌缓存在服务器上并保存在数据库中.因此,当该用户被吊销时,我可以简单地从缓存和数据库中删除该用户条目,以阻止该用户访问受保护的资源.

但是下面这2个答案表明这不是oauth2的设计方式:

撤消OAuthBearerAuthentication的访问令牌

OAuth2-带有刷新令牌的不必要的复杂性

所以我的问题是:

1)为什么不认为缓存访问令牌比刷新令牌机制更好,而且是一种不好的方法?

我的第二个问题是基于 @Hans Z.给出的以下答案:

这必然涉及资源服务器(RS)咨询 授权服务器(AS),这是一个巨大的开销.

2)在撤销用户访问权限的情况下,RS为什么会咨询AS,因为AS仅用于验证用户并根据此解决方案

这里似乎有两个不同的问题:关于访问令牌和关于角色的大列表.

访问令牌

OAuth2被设计为能够处理高负载,这需要权衡取舍.特别是这就是为什么OAuth2一方面明确地将资源服务器"和授权服务器"角色分开,另一方面将访问令牌"和刷新令牌"分开的原因.如果对于每个请求都必须检查用户授权,则意味着授权服务器应该能够处理系统中的所有请求.对于高负载系统,这是不可行的.

OAuth2允许您在性能和安全性之间进行以下 权衡 :授权服务器生成可以由资源服务器验证而无需访问授权服务器的访问令牌. (在Authorization Server的整个生命期内或至少不超过一次).这有效地缓存了授权信息.因此,通过这种方式,您可以大大减少授权服务器上的负载.缺点还是与高速缓存相同:授权信息可能会停滞.通过更改访问令牌的生存时间,您可以调整性能与安全性的平衡.

如果您执行微服务体系结构,其中每个服务都具有自己的存储并且不访问彼此的存储,那么这种方法也可能会有所帮助.

不过,如果您没有很多负载,并且只有一个资源服务器,而不是使用不同技术实现的大量不同服务,那么没有什么会阻止您实际对每个请求进行全面验证. IE.是的,您可以将访问令牌存储在数据库中,在对资源服务器的每次访问中对其进行验证,并在删除用户后删除所有访问令牌,等等.但是正如@Evk所注意到的,如果这是您的情况-OAuth2对您来说是一个过大的选择

角色大列表

AFAIU OAuth2没有为用户角色提供显式功能.有作用域"功能也可能用于角色,并且它的典型实现方式对于250个角色将产生太长的字符串. OAuth2仍然没有明确指定访问令牌的任何特定格式,因此您可以创建一个自定义令牌,以将角色信息作为位掩码保存.使用base-64编码,您可以将6个角色转换成一个字符(64 = 2 ^ 6).因此250-300个角色将可以管理40-50个字符.

JWT

由于您可能仍然需要一些自定义令牌,因此您可能会对 JSON Web令牌(又名JWT)感兴趣.简而言之,JWT允许您指定自定义的其他有效负载(私有声明),并将角色的位掩码放在此处.

如果您确实不需要任何OAuth2高级功能(例如作用域),则实际上可以单独使用JWT,而无需使用完整的OAuth2.尽管应该仅通过theis内容来验证JWT令牌,但是您仍然可以将它们存储在本地数据库中,并对数据库进行其他验证(就像您要使用访问刷新令牌一样).


更新2017年12月1日

如果要使用OWIN OAuth基础结构,则可以通过AccessTokenFormat来提供自定义格式程序,从而自定义令牌格式. security.oauth.oauthbearerauthenticationoptions.accesstokenformat(v = vs.113).aspx"rel =" noreferrer> OAuthBearerAuthenticationOptions

  1. 定义您的CustomRoles枚举,其中列出了您拥有的所有角色

 [Flags]
public enum CustomRoles
{
    Role1,
    Role2,
    Role3,

    MaxRole // fake, for convenience
}
 

  1. 根据上面定义的CustomRoles,创建EncodeRolesDecodeRoles方法以在角色的IEnumerable<string>格式和base64编码的位掩码之间进行转换,例如:

     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. SecureDataFormat<AuthenticationTicket>的自定义实现中使用这些方法.为简单起见,在此草图中,我将大部分工作委托给标准的OWIN组件,并仅实现创建另一个AuthenticationTicket并使用标准DataSerializers.TicketCustomTicketSerializer.显然,这不是最有效的方法,但它显示了您可以执行的操作:

 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. 在您的Startup.cs中,将OWIN配置为使用您的自定义格式,例如:

 var customTicketFormat = CustomTicketSerializer.CreateCustomTicketFormat(app);
OAuthBearerOptions.AccessTokenFormat = customTicketFormat;
OAuthServerOptions.AccessTokenFormat = customTicketFormat;
 

  1. 在您的OAuthAuthorizationServerProvider中,为分配给用户的每个角色将ClaimTypes.Role添加到ClaimsIdentity.

  2. 在您的控制器中使用标准的AuthorizeAttribute,例如

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

为了方便和安全起见,您可以将AuthorizeAttribute类子类化为接受CustomRoles枚举而不是字符串作为角色配置.

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.

解决方案

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.

这篇关于为什么在oauth2中认为缓存访问令牌是错误的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆