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

84

我正在使用附带有OWIN中间件的Visual Studio 2013 Web Api 2模板进行用户认证等操作。

注意到在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
 };

这不适合我的最新项目。我想发放短期的bearer_token,并且可以使用refresh_token来刷新。

我已经搜索了很多,但没有找到任何有用的东西。

所以现在我做到这个地步了。我已经编写了一个实现了IAuthenticationTokenProvider接口的RefreshTokenProvider

它可以作为OAuthAuthorizationServerOptions类中的RefreshTokenProvider属性使用。

    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

我真的需要一些阅读材料/文档,因为我不想做错,想要遵循某种标准。


7
这里有一篇非常好的教程,介绍了如何使用 Owin 和 OAuth 实现刷新令牌:http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/。 - Philip Bergström
4个回答

84

刚刚使用了 Bearer (在下文中称为 access_token) 和 Refresh Tokens 来实现我的 OWIN 服务。我认为你可以使用不同的工作流程,因此它取决于您要使用的工作流程如何设置 access_token 和 refresh_token 的过期时间。

我将在下面描述两种工作流程 A 和 B(我建议您使用工作流程 B):

A) access_token 和 refresh_token 的过期时间默认相同,为 1200 秒或 20 分钟。此流程需要您的客户端首先发送 client_id 和 client_secret 登录数据来获取 access_token、refresh_token 和 expiration_time。使用 refresh_token,现在可以获得一个新的 access_token,有效期为 20 分钟(或者您将 AccessTokenExpireTimeSpan 设置为 OAuthAuthorizationServerOptions 中的任何其他值)。由于 access_token 和 refresh_token 的过期时间相同,您的客户端有责任在过期之前获取新的 access_token!例如,您的客户端可以向令牌端点发送带有请求体的刷新 POST 调用(请注意:生产环境中应使用 https)。

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

为了防止令牌过期,您需要在19分钟后获取一个新的令牌。

B) 在这个流程中,您希望访问令牌具有短期过期时间,刷新令牌具有长期过期时间。 假设为测试目的,您将访问令牌设置为在10秒钟内过期(AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10)),而刷新令牌则设置为5分钟。 现在进入有趣的部分——设置刷新令牌的过期时间:您可以在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请求。调用的body部分可能如下所示:grant_type=refresh_token&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx 重要的一点是,您可能不仅想在CreateAsync函数中使用此代码,还要在Create函数中使用它。因此,您应该考虑使用自己的函数(例如称为CreateTokenInternal)来处理上述代码。 这里您可以找到不同流程的实现,包括refresh_token流程(但未设置refresh_token的过期时间)。 这是一个关于IAuthenticationTokenProvider的样例实现在Github上(同时设置了refresh_token的过期时间)。
对不起,我不能提供比OAuth规范和微软API文档更多的材料。我会把链接放在这里,但我的声望不允许我发布超过2个链接...
我希望这能帮助其他人在尝试实现不同于access_token的refresh_token过期时间的OAuth2.0时节省一些时间。除了上面链接的thinktecture的示例实现外,我找不到网上其他的实现,这让我花费了一些时间来研究,直到它为我工作。
新信息:在我的情况下,我有两种不同的可能性来接收令牌。一种是接收有效的access_token。在那里,我必须发送一个格式为application/x-www-form-urlencoded的字符串体POST调用,其中包含以下数据。
client_id=YOURCLIENTID&grant_type=password&username=YOURUSERNAME&password=YOURPASSWORD

第二种情况是如果access_token不再有效,我们可以尝试使用refresh_token。方法是发送一个带有字符串主体的POST调用,格式为application/x-www-form-urlencoded,其中包含以下数据grant_type=refresh_token&client_id=YOURCLIENTID&refresh_token=YOURREFRESHTOKENGUID


1
你的其中一条评论说“考虑仅存储句柄的哈希值”,难道这个评论不应该适用于上面的那行代码吗?票证保存了原始的GUID,但我们只在“RefreshTokens”中存储GUID的哈希值,因此如果“RefreshTokens”泄漏,攻击者无法使用该信息! - esskar
好像是这样的;问OA:https://github.com/thinktecture/Thinktecture.IdentityModel/commit/108294df9500f15ed4a19e3cc1a6f204b48fb08e - esskar
1
如流程B所述,您可以使用AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(60)设置access_token的过期时间为一小时或FromWHATEVER以设置access_token过期的时间。但请注意,如果您在流程中使用refresh_token,则refresh_token的过期时间应该比access_token的过期时间更长。因此,例如,对于access_token使用24小时,对于refresh_token使用2个月。您可以在OAuth配置中设置access_token的过期时间。 - Freddy
12
不要使用GUID或其哈希值作为令牌,这不安全。请使用System.Cryptography命名空间生成随机字节数组,并将其转换为字符串作为令牌。否则,您的刷新令牌可能会被暴力攻击猜测。 - Bon
1
@Bon 你要暴力猜测一个 GUID 吗?在攻击者发出少数请求之前,你的速率限制器应该会启动。如果没有,那也只是一个 GUID 而已。 - lonix
显示剩余7条评论

46

您需要实现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()
};

这将每次创建并返回一个新的刷新令牌,即使您可能只对返回一个新的访问令牌而不是新的刷新令牌感兴趣。例如,在使用刷新令牌而不是凭据(用户名/密码)调用访问令牌时。有没有办法避免这种情况? - Mattias
你可以这样做,但并不美观。context.OwinContext.Environment 包含一个 Microsoft.Owin.Form#collection 键,它提供了一个 FormCollection,在其中你可以找到授权类型并相应地添加令牌。这会泄露实现细节,可能会在未来的更新中出现问题,并且我不确定它是否可移植到其他 OWIN 主机。 - hvidgaard
3
为避免每次都发放新的刷新令牌,可以通过从 OwinRequest 对象中读取 "grant_type" 值来实现,如下所示:var form = await context.Request.ReadFormAsync(); var grantType = form.GetValue("grant_type");然后,只有在授权类型不是 "refresh_token" 时才发放刷新令牌。 - Duy
1
@mattias 在这种情况下,您仍然需要返回一个新的刷新令牌。否则,在第一次刷新后,客户端将陷入困境,因为第二个访问令牌过期了,他们没有办法进行刷新,而不必再次提示输入凭据。 - Eric Eskildsen

9

我认为你不应该使用数组来维护令牌,也不需要将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);
    }
}

3

Freddy的回答帮助了我很多,使得这个工作起来非常顺利。为了完整起见,这里是如何实现令牌哈希处理的:

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
3
原始解决方案存储了Guid。使用哈希后,我们不会保存明文令牌,而是保存其哈希值。例如,如果您将令牌存储在数据库中,则最好存储哈希值。如果数据库被攻击,只要加密了令牌,它们就无法使用。 - Knelis
不仅要防范外部威胁,还要防止有权访问数据库的员工窃取令牌。 - lonix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接