OWIN ASP.NET - 如果访问令牌已过期,则无法使用刷新令牌生成访问令牌

OWIN ASP.NET - Cant generate Access Token using Refresh Token if Access Token is expired

当我尝试在访问令牌过期之前使用刷新令牌生成访问令牌时,系统会生成一个新的访问令牌并且一切正常。但是如果访问令牌过期,请求 return invalid_grant.

GrantRefreshToken 中的 Validated() 方法不是使用我从之前的访问令牌存储在字典中的身份生成访问令牌吗?

如果最后一个访问令牌尚未过期,我如何阻止客户端使用相同的刷新令牌请求新的访问令牌?

这是我的代码:

Startup.cs:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,

            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5),

            Provider = new OAuthProvider(),
            RefreshTokenProvider = new RefreshTokenProvider()
        });
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        app.UseWebApi(config);
    }
}

OAuthProvider.cs:

public class OAuthProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (context.UserName == "admin" && context.Password == "123456")
        {
            var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));

            var ticket = new AuthenticationTicket(claimsIdentity, null);
            context.Validated(ticket);
        }
        return Task.FromResult<object>(null);
    }

    public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
    {
        context.Validated(context.Ticket);
        return Task.FromResult<object>(null);
    }
}

RefreshTokenProvider.cs:

public class RefreshTokenProvider : AuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, AuthenticationTicket> _refreshTokens = new ConcurrentDictionary<string, AuthenticationTicket>();

    public override Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var guid = Guid.NewGuid().ToString();
        _refreshTokens.TryAdd(guid, context.Ticket);

        context.SetToken(guid);
        return Task.FromResult<object>(null);
    }

    public override Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        if (_refreshTokens.TryRemove(context.Token, out AuthenticationTicket ticket))
        {
            context.SetTicket(ticket);
        }
        return Task.FromResult<object>(null);
    }
}

抱歉英语不好,希望你能理解!

编辑:

好吧,我修改了代码以实现数据库支持并将刷新令牌过期时间设置为 5 分钟。出于测试目的,过期时间很小。

结果如下:

OAuthProvider.cs:

public class OAuthProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        try
        {
            var account = AccountRepository.Instance.GetByUsername(context.UserName);
            if (account != null && Global.VerifyHash(context.Password, account.Password))
            {
                var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, account.Username));
                claimsIdentity.AddClaim(new Claim("DriverId", account.DriverId.ToString()));

                var newTicket = new AuthenticationTicket(claimsIdentity, null);
                context.Validated(newTicket);
            }
        }
        catch { }

        return Task.FromResult<object>(null);
    }

    public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }
}

RefreshTokenProvider.cs:

public class RefreshTokenProvider : AuthenticationTokenProvider
{
    public override Task CreateAsync(AuthenticationTokenCreateContext context)
    {
        var refreshToken = new TokenModel()
        {
            Subject = context.Ticket.Identity.Name,
            Token = GenerateToken(),
            IssuedUtc = DateTime.UtcNow,
            ExpiresUtc = DateTime.UtcNow.AddMinutes(5)
        };

        context.Ticket.Properties.IssuedUtc = refreshToken.IssuedUtc;
        context.Ticket.Properties.ExpiresUtc = refreshToken.ExpiresUtc;

        refreshToken.Ticket = context.SerializeTicket();

        try
        {
            TokenRepository.Instance.Insert(refreshToken);
            context.SetToken(refreshToken.Token);
        }
        catch { }

        return Task.FromResult<object>(null);
    }

    public override Task ReceiveAsync(AuthenticationTokenReceiveContext context)
    {
        try
        {
            var refreshToken = TokenRepository.Instance.Get(context.Token);
            if (refreshToken != null)
            {
                if (TokenRepository.Instance.Delete(refreshToken))
                {
                    context.DeserializeTicket(refreshToken.Ticket);
                }
            }
        }
        catch { }

        return Task.FromResult<object>(null);
    }

    private string GenerateToken()
    {
        HashAlgorithm hashAlgorithm = new SHA256CryptoServiceProvider();

        byte[] byteValue = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString("N"));
        byte[] byteHash = hashAlgorithm.ComputeHash(byteValue);

        return Convert.ToBase64String(byteHash);
    }
}

感谢支持!

刷新令牌似乎与访问令牌具有相同的到期时间。所以你需要延长刷新令牌的有效期:

public override Task CreateAsync(AuthenticationTokenCreateContext context)
{
    var form = context.Request.ReadFormAsync().Result;
    var grantType = form.GetValues("grant_type");

    if (grantType[0] != "refresh_token")
    {
        ...

        // One day
        int expire = 24 * 60 * 60;
        context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(DateTime.Now.AddSeconds(expire));
    }
    base.Create(context);
}

-- 更新--

我已经更新了代码以回答您评论中的问题。这比你要求的更多。但我认为这将有助于解释。

这完全取决于要求和选择的策略。代码解释:

每次发布访问令牌时,您还会点击这段代码,它会添加一个刷新令牌。在这个策略中,只有当 grant_type 不是 'refresh_token' 时,我才会发布一个新的刷新令牌。这意味着刷新令牌有时会过期,用户必须重新登录。

在示例中,用户必须每天重新登录。但是,如果用户在 refresh_token 到期之前登录,则会发出一个新的刷新令牌。这样刷新令牌就有了绝对的过期时间,强制用户每天至少登录一次。

如果你想要'sliding expiration',你可以在每次发布访问令牌时添加一个刷新令牌。请注意,用户可能永远不必再次登录,除非刷新令牌在刷新前过期。

无论如何,当 grant_type 不是 refresh_token 时,我不会阻止用户创建访问令牌。因为那是用户交互。访问令牌具有非常有限的 window(想想几小时或几分钟,而不是几天),所以它很快就会过期。您不希望每次在 header 中收到访问令牌时都对其进行验证,因为这意味着您需要为每次调用检查数据库。

而是考虑一种使 'old' 刷新令牌无效的策略。您可以将最后一个刷新令牌的散列版本保存在数据库中。如果不匹配,则返回 'invalid_grant' 错误。