c# - OWIN ASP.NET - 避免在 Web Api 中没有身份的同一帐户多次登录

标签 c# asp.net authentication asp.net-web-api owin

我想知道如何阻止用户同时使用多个刷新 token 。让我解释一下:

  • 有些用户使用凭据请求新的访问 token 。
  • 凭据没问题,因此身份验证服务器会返回一个访问 token 和一个刷新 token 。
  • 当访问 token 过期时,用户必须使用新的刷新 token 而不是凭据来获取新的访问 token 。

问题在于,如果另一个用户使用相同的凭据登录,则会为相同的身份生成另一个刷新 token 。所以,我想要做的是:如果有人使用一些具有事件刷新 token 的凭据再次登录,而不是生成一个新的,替换现有的,或者删除它并插入新的。因此,当访问 token 过期时,先前的用户将断开连接,因为刷新 token 将不再存在。

此外,我如何实现一些服务来销毁身份验证服务器中的刷新 token ?因此用户可以调用它来断开他的帐户,而不仅仅是删除 cookie 并等待它过期。

这是我的代码:

Startup.cs:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        HttpConfiguration config = new HttpConfiguration();

        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}"
        );

        app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,

            TokenEndpointPath = new PathString("/auth"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(1),

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

还有一个问题:如何在 catch 中抛出内部服务器错误?因为它实际上返回的是 invalid_grant,但 catch 意味着数据库错误,而不是无效的凭据或 token 。

感谢您的帮助,抱歉英语不好。希望你明白!

最佳答案

考虑以下几点:

  1. 将访问 token 过期设置为较小的值(分钟/小时)
  2. 仅在用户使用凭据 (grant_type=password) 登录时发出刷新 token 。这是一个新的刷新 token 。
  3. 将"new"刷新 token 加密存储在数据库中,替换“当前”刷新 token 。

您不必阻止用户。访问 token 很快就会过期,只需确保在刷新 token 时不会发出新的访问 token 。您可以通过检查刷新 token 来执行此操作。如果加密的刷新 token 与数据库中的加密刷新 token 不匹配,将返回“invalid_grant”。用户只有一个选择:重新登录。

如果用户使用凭据登录,则更新 token (也在数据库中)。这将自动使“旧”刷新 token 失效。

您可以在 RefreshTokenProvider.CreateAsync 中实现第 2 点和第 3 点。一些伪代码:

// using Microsoft.AspNet.Identity;

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

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

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

        // Store the encrypted token in the database
        var currentUser = context.Ticket.Identity.GetUserId();
        TokenRepository.Instance.EncryptAndSaveTokenInDatabase(context.Token, currentUser);
    }
    base.Create(context);
}

关于错误,只返回invalid_grant。您希望数据库多久失败一次?客户端将期望登录或收到“invalid_grant”。它知道如何处理(重定向到登录页面)。客户端不必知道存在数据库错误。如果您需要其他信息,可以将其记录在后端。

关于c# - OWIN ASP.NET - 避免在 Web Api 中没有身份的同一帐户多次登录,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44274293/

相关文章:

c# - 阻止 HttpWebRequest 检查我的负载

c# - 奇怪的 TimeSpan.ToString 输出

javascript - 调用函数仅一次返回 false 后,表单不执行提交

javascript - 使用 jQuery 和 Bootstrap 设计复选框按钮的样式

c# - 如何在没有 AAD 访问权限的 Microsoft 团队聊天机器人中实现用户身份验证?

c# - LINQ:元组列表到列表元组

c# - 无法引用该项目。引用的项目针对不同的框架系列

asp.net - 从 MySQL 读取记录集的特殊字符问题

javascript - 这是一种安全的方式吗? Redux React 身份验证

android - smackx 无法登录到 ejabberd socketexception 发送失败管道损坏