OWIN 安全 - 如何实现 OAuth2 刷新令牌

Posted

技术标签:

【中文标题】OWIN 安全 - 如何实现 OAuth2 刷新令牌【英文标题】:OWIN Security - How to Implement OAuth2 Refresh Tokens 【发布时间】:2014-01-05 10:09:05 【问题描述】:

我正在使用 Visual Studio 2013 附带的 Web Api 2 模板,它有一些 OWIN 中间件来进行用户身份验证等。

OAuthAuthorizationServerOptions 中,我注意到 OAuth2 服务器设置为分发 14 天后过期的令牌

 OAuthOptions = new OAuthAuthorizationServerOptions
 
      TokenEndpointPath = new PathString("/api/token"),
      Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
      AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
      AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
      AllowInsecureHttp = true
 ;

这不适合我的最新项目。我想分发可以使用 refresh_token 刷新的短暂的 bearer_tokens

我在谷歌上搜索了很多,但找不到任何有用的东西。

所以这就是我设法达到的程度。我现在已经达到了“我现在做WTF”的地步。

我写了一个RefreshTokenProvider,它根据OAuthAuthorizationServerOptions 类的RefreshTokenProvider 属性实现IAuthenticationTokenProvider

    public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
    
       private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

        public async Task CreateAsync(AuthenticationTokenCreateContext context)
        
            var guid = Guid.NewGuid().ToString();


            _refreshTokens.TryAdd(guid, context.Ticket);

            // hash??
            context.SetToken(guid);
        

        public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        
            AuthenticationTicket ticket;

            if (_refreshTokens.TryRemove(context.Token, out ticket))
            
                context.SetTicket(ticket);
            
        

        public void Create(AuthenticationTokenCreateContext context)
        
            throw new NotImplementedException();
        

        public void Receive(AuthenticationTokenReceiveContext context)
        
            throw new NotImplementedException();
        
    

    // Now in my Startup.Auth.cs
    OAuthOptions = new OAuthAuthorizationServerOptions
    
        TokenEndpointPath = new PathString("/api/token"),
        Provider = new ApplicationOAuthProvider(PublicClientId,UserManagerFactory) ,
        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
        AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(2),
        AllowInsecureHttp = true,
        RefreshTokenProvider = new RefreshTokenProvider() // This is my test
    ;

所以现在当有人请求bearer_token 我现在发送refresh_token,这很棒。

那么现在我如何使用这个 refresh_token 来获得一个新的bearer_token,大概我需要向我的令牌端点发送一个请求并设置一些特定的 HTTP 标头?

只是在我输入时大声思考...我应该在我的SimpleRefreshTokenProvider 中处理 refresh_token 到期吗?客户如何获得新的refresh_token

我真的可以阅读一些材料/文档,因为我不想弄错,并且想遵循某种标准。

【问题讨论】:

有一个关于使用 Owin 和 OAuth 实现刷新令牌的精彩教程:bitoftech.net/2014/07/16/… 【参考方案1】:

我认为您不应该使用数组来维护令牌。您也不需要 guid 作为令牌。

您可以轻松使用 context.SerializeTicket()。

请看我下面的代码。

public class RefreshTokenProvider : IAuthenticationTokenProvider

    public async Task CreateAsync(AuthenticationTokenCreateContext context)
    
        Create(context);
    

    public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    
        Receive(context);
    

    public void Create(AuthenticationTokenCreateContext context)
    
        object inputs;
        context.OwinContext.Environment.TryGetValue("Microsoft.Owin.Form#collection", out inputs);

        var grantType = ((FormCollection)inputs)?.GetValues("grant_type");

        var grant = grantType.FirstOrDefault();

        if (grant == null || grant.Equals("refresh_token")) return;

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);

        context.SetToken(context.SerializeTicket());
    

    public void Receive(AuthenticationTokenReceiveContext context)
    
        context.DeserializeTicket(context.Token);

        if (context.Ticket == null)
        
            context.Response.StatusCode = 400;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "invalid token";
            return;
        

        if (context.Ticket.Properties.ExpiresUtc <= DateTime.UtcNow)
        
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            context.Response.ReasonPhrase = "unauthorized";
            return;
        

        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(Constants.RefreshTokenExpiryInDays);
        context.SetTicket(context.Ticket);
    

【讨论】:

【参考方案2】:

Freddy's answer 帮我完成了这项工作。为了完整起见,以下是实现令牌散列的方法:

private string ComputeHash(Guid input)

    byte[] source = input.ToByteArray();

    var encoder = new SHA256Managed();
    byte[] encoded = encoder.ComputeHash(source);

    return Convert.ToBase64String(encoded);

CreateAsync:

var guid = Guid.NewGuid();
...
_refreshTokens.TryAdd(ComputeHash(guid), refreshTokenTicket);
context.SetToken(guid.ToString());

ReceiveAsync:

public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)

    Guid token;

    if (Guid.TryParse(context.Token, out token))
    
        AuthenticationTicket ticket;

        if (_refreshTokens.TryRemove(ComputeHash(token), out ticket))
        
            context.SetTicket(ticket);
        
    

【讨论】:

哈希在这种情况下有何帮助? @Ajaxe:原始解决方案存储了 Guid。通过散列,我们保留的不是纯文本标记,而是它的散列。例如,如果您将令牌存储在数据库中,则最好存储散列。如果数据库遭到破坏,则令牌只要被加密就无法使用。 不仅可以抵御外部威胁,还可以防止员工(有权访问数据库)窃取令牌。【参考方案3】:

刚刚使用 Bearer(以下称为 access_token)和刷新令牌实现了我的 OWIN 服务。我对此的见解是,您可以使用不同的流程。因此,这取决于您要使用的流程如何设置 access_token 和 refresh_token 到期时间。

我将在下面描述两个流程 AB(我建议您想要的是流程B):

A) access_token 和 refresh_token 的过期时间与默认的 1200 秒或 20 分钟相同。此流程需要您的客户端首先发送带有登录数据的 client_id 和 client_secret 以获取 access_token、refresh_token 和 expire_time。使用 refresh_token 现在可以获得一个新的 access_token 20 分钟(或任何您在 OAuthAuthorizationServerOptions 中设置的 AccessTokenExpireTimeSpan )。因为access_token和refresh_token的过期时间是一样的,所以你的客户端有责任在过期时间之前拿到一个新的access_token!例如。您的客户端可以使用正文向您的令牌端点发送刷新 POST 调用(备注:您应该在生产中使用 https)

grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxx

在 e.g. 之后获得一个新的令牌19 分钟以防止令牌过期。

B) 在此流程中,您希望 access_token 短期到期,而 refresh_token 长期到期。让我们假设出于测试目的,您将 access_token 设置为 10 秒后过期 (AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)) 并将 refresh_token 设置为 5 分钟。现在到了设置 refresh_token 过期时间的有趣部分:您可以在 SimpleRefreshTokenProvider 类的 createAsync 函数中执行此操作,如下所示:

var guid = Guid.NewGuid().ToString();


        //copy properties and set the desired lifetime of refresh token
        var refreshTokenProperties = new AuthenticationProperties(context.Ticket.Properties.Dictionary)
        
            IssuedUtc = context.Ticket.Properties.IssuedUtc,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5) //SET DATETIME to 5 Minutes
            //ExpiresUtc = DateTime.UtcNow.AddMonths(3) 
        ;
        /*CREATE A NEW TICKET WITH EXPIRATION TIME OF 5 MINUTES 
         *INCLUDING THE VALUES OF THE CONTEXT TICKET: SO ALL WE 
         *DO HERE IS TO ADD THE PROPERTIES IssuedUtc and 
         *ExpiredUtc to the TICKET*/
        var refreshTokenTicket = new AuthenticationTicket(context.Ticket.Identity, refreshTokenProperties);

        //saving the new refreshTokenTicket to a local var of Type ConcurrentDictionary<string,AuthenticationTicket>
        // consider storing only the hash of the handle
        RefreshTokens.TryAdd(guid, refreshTokenTicket);            
        context.SetToken(guid);

现在,当access_token 过期时,您的客户端可以向您的令牌端点发送带有 refresh_token 的 POST 调用。调用的正文部分可能如下所示:grant_type=refresh_token&amp;client_id=xxxxxx&amp;refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

重要的一点是,您可能不仅希望在 CreateAsync 函数中使用此代码,而且还希望在 Create 函数中使用此代码。因此,您应该考虑为上述代码使用您自己的函数(例如,称为 CreateTokenInternal)。 Here you can find implementations of different flows including refresh_token flow(但没有设置refresh_token的过期时间)

Here is one sample implementation of IAuthenticationTokenProvider on github(带有设置refresh_token的过期时间)

很抱歉,除了 OAuth 规范和 Microsoft API 文档之外,我无法提供更多材料。我会在这里发布链接,但我的声誉不允许我发布超过 2 个链接....

我希望这可以帮助其他一些人在尝试使用与 access_token 过期时间不同的 refresh_token 过期时间来实现 OAuth2.0 时腾出时间。我在网络上找不到示例实现(上面链接的 thinktecture 除外),我花了几个小时的调查才发现它对我有用。

新信息:就我而言,我有两种不同的可能性来接收令牌。一种是接收有效的 access_token。在那里,我必须发送一个带有字符串正文的 POST 调用,格式为 application/x-www-form-urlencoded,并带有以下数据

client_id=YOURCLIENTID&grant_type=password&username=YOURUSERNAME&password=YOURPASSWORD

其次,如果 access_token 不再有效,我们可以通过发送带有字符串正文的 POST 调用来尝试 refresh_token,格式为 application/x-www-form-urlencoded,并带有以下数据 grant_type=refresh_token&amp;client_id=YOURCLIENTID&amp;refresh_token=YOURREFRESHTOKENGUID

【讨论】:

您的一个 cmets 说“考虑只存储句柄的哈希”,该评论不应该适用于上面的行吗?票据持有原始 guid,但我们只将 guid 的哈希存储在 RefreshTokens 中,所以如果 RefreshTokens 泄露,攻击者无法使用该信息!? 好像是这样;问OA:github.com/thinktecture/Thinktecture.IdentityModel/commit/… 如流程 B 中所述,您可以使用 AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(60) 为一小时或 FromWHATEVER 设置 access_token 的过期时间为您希望 access_token 过期的时间。但请注意,如果您在流程中使用 refresh_token,则 refresh_token 的到期时间应高于您的 access_token。例如,access_token 为 24 小时,refresh_token 为 2 个月。您可以在 OAuth 配置中设置 access_token 的过期时间。 不要将 Guid 用于您的令牌或它们的哈希值,这不安全。使用 System.Cryptography 命名空间生成随机字节数组并将其转换为字符串。否则你的刷新令牌可能会被暴力攻击猜到。 @Bon 你要蛮力猜一个Guid?您的速率限制器应该在攻击者甚至可以发布少量请求之前发挥作用。如果不是,它仍然是一个 Guid。【参考方案4】:

您需要实现 RefreshTokenProvider。 首先为 RefreshTokenProvider 创建类,即。

public class ApplicationRefreshTokenProvider : AuthenticationTokenProvider

    public override void Create(AuthenticationTokenCreateContext context)
    
        // Expiration time in seconds
        int expire = 5*60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
        context.SetToken(context.SerializeTicket());
    

    public override void Receive(AuthenticationTokenReceiveContext context)
    
        context.DeserializeTicket(context.Token);
    

然后将实例添加到 OAuthOptions

OAuthOptions = new OAuthAuthorizationServerOptions

    TokenEndpointPath = new PathString("/authenticate"),
    Provider = new ApplicationOAuthProvider(),
    AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(expire),
    RefreshTokenProvider = new ApplicationRefreshTokenProvider()
;

【讨论】:

这将每次创建并返回一个新的刷新令牌,即使您可能只希望返回一个新的访问令牌而不是一个新的刷新令牌。例如 wen 调用访问令牌但使用刷新令牌而不是凭据(用户名/密码)。有没有办法避免这种情况? 你可以,但它并不漂亮。 context.OwinContext.Environment 包含一个 Microsoft.Owin.Form#collection 键,它为您提供了一个 FormCollection,您可以在其中找到授权类型并相应地添加一个令牌。它正在泄漏实现,它可能会在未来的更新中随时中断,我不确定它是否可以在 OWIN 主机之间移植。 您可以通过从 OwinRequest 对象读取“grant_type”值来避免每次都发出新的刷新令牌,如下所示:var form = await context.Request.ReadFormAsync();var grantType = form.GetValue("grant_type"); 然后如果授予类型不是“则发出刷新令牌”刷新令牌” @mattias 在这种情况下,您仍然希望返回一个新的刷新令牌。否则客户端在第一次刷新后会陷入困境,因为第二个访问令牌过期了,如果不再次提示输入凭据,他们就无法刷新。

以上是关于OWIN 安全 - 如何实现 OAuth2 刷新令牌的主要内容,如果未能解决你的问题,请参考以下文章

Spring Oauth2 - 重新加载主体

安全性 - JWT 和 Oauth2(刷新令牌)

Microsoft.Owin.Security 自定义AuthenticationHandler 实现oauth2 的授权码模式

如何在 Asp.Net Web API 2 中使用 Owin OAuth2 修改令牌端点响应正文

使用Owin中间件搭建OAuth2.0认证授权服务器

如何从 OAuth 客户端凭据开始使用 OWIN Oauth 保护 WebApi?