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


80

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

OAuthAuthorizationServerOptionsI中,我注意到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_tokens,可以使用refresh_token

我已经进行了大量谷歌搜索,找不到任何有用的信息。

这就是我设法取得的成就。我现在已经达到“现在就做WTF”这一点。

我写了一个根据类的属性RefreshTokenProvider实现的:IAuthenticationTokenProviderRefreshTokenProviderOAuthAuthorizationServerOptions

    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标头的令牌端点)?

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

我真的可以使用一些阅读材料/文档,因为我不想弄错这一点,而是想遵循某种标准。


7
有关于执行使用Owin和OAuth刷新标记一个伟大的教程:bitoftech.net/2014/07/16/...
菲利普·伯格斯特龙

Answers:


75

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

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

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

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

例如在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&client_id=xxxxxx&refresh_token=xxxxxxxx-xxxx-xxxx-xxxx-xx

重要的一件事是,您可能不仅要在CreateAsync函数中使用此代码,还要在Create函数中使用此代码。因此,您应该考虑为上述代码使用自己的函数(例如,称为CreateTokenInternal)。 在这里,您可以找到不同流程的实现,包括refresh_token流(但未设置refresh_token的到期时间)

这是GitHub上IAuthenticationTokenProvider的一个示例实现(设置了refresh_token的到期时间)

很抱歉,除了OAuth规范和Microsoft API文档之外,我无法提供更多其他材料。我会在此处发布链接,但是我的声誉不允许我发布两个以上的链接...。

我希望这可以帮助其他人在尝试以不同于access_token到期时间的refresh_token到期时间实现OAuth2.0时节省时间。我无法在网络上找到示例实现(除了上面链接的thinktecture之一),这花了我几个小时的研究才对我有用。

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

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

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


1
您的评论之一说“考虑只存储句柄的哈希值”,该评论不应该应用于上面的行吗?票证包含原始guid,但我们仅将guid的哈希存储在中RefreshTokens,因此如果RefreshTokens泄漏,攻击者将无法使用该信息!
esskar 2014年


1
如流程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的到期时间。
弗雷迪(Freddy)2015年

12
不要将Guid用于令牌或哈希,这是不安全的。使用System.Cryptography命名空间生成随机字节数组,并将其转换为字符串。否则,暴力攻击可能会猜测您的刷新令牌。
Bon

1
@Bon您要蛮力猜测Guid吗?您的速率限制器应在攻击者发布少量请求之前启动。如果没有,它仍然是Guid。
lonix

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()
};

这将每次都创建并返回一个新的刷新令牌,即使您可能只想返回一个新的访问令牌而不是一个新的刷新令牌。例如,wen要求访问令牌,但带有刷新令牌,而不是凭据(用户名/密码)。反正有避免这种情况吗?
马蒂亚斯(Mattias)

可以,但这并不漂亮。该context.OwinContext.Environment包含一个Microsoft.Owin.Form#collection关键它给你FormCollection,你可以找到授权类型,并添加相应的令牌。它正在泄漏实现,它可能会在将来的更新中随时中断,并且我不确定它是否可以在OWIN主机之间移植。
hvidgaard '16

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);
    }
}

2

弗雷迪的回答对我起到了很大帮助作用。为了完整起见,以下是实现令牌哈希的方法:

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);
        }
    }
}

在这种情况下,哈希如何提供帮助?
阿贾克斯

3
@Ajaxe:原始解决方案存储了Guid。使用散列时,我们不保留纯文本令牌,而是保留其散列。例如,如果将令牌存储在数据库中,则最好存储哈希。如果数据库遭到破坏,则令牌只要被加密就无法使用。
Knelis '16

不仅可以防御外部威胁,还可以防止员工(有权访问数据库)窃取令牌。
lonix
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.