【问题标题】:Web API Preventing Refresh Tokens from Leakage - ImplementationWeb API 防止刷新令牌泄漏 - 实现
【发布时间】:2018-08-19 20:28:47
【问题描述】:

我正在处理 JWT 及其刷新令牌,但找不到一个可以同时提供性能和安全性的良好工作示例。

性能:: 每次刷新令牌时都不能访问数据库。

安全性:: 由于生命周期长,刷新令牌应该是超级机密而不是访问令牌。

所以我尝试结合使用内存缓存和过期令牌声明来实现我自己的:

第 1 步。

a) 成功登录后,生成具有 JwtRegisteredClaimNames.Jti 声明类型中唯一 GUID 的访问令牌..

b) 然后生成refresh-token,并以关联的jti access-token值(唯一GUID)作为key保存在memoryCache中

c) 两者都发送到客户端应用程序并存储在 localStorage 中。

第 2 步。

a)access-token 过期后,access-token 和 refresh-token 都会发送到刷新控制器。

b) 然后 jti 在过期令牌中声明作为缓存键发送到 memoryCache 以从内存中获取刷新令牌。

c) 检查 -send refresh-token 和 -in-memory refresh-token 的相等性后,如果相等,则生成 access-token 和 refresh-token 的新实例并将其发送回客户端应用程序。

AuthService.cs

 private readonly IConfiguration _configuration;
    private readonly IMemoryCache _memoryCache;
    private readonly Claim _jtiClaim;
    public AuthService(IConfiguration configuration, IMemoryCache memoryCache)
    {
        _configuration = configuration;
        _memoryCache = memoryCache;
        _jtiClaim = new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString());

    }

    public string GenerateAccessToken(IList<Claim> claims)
    {
        claims.Add(_jtiClaim);
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtConfiguration:JwtKey"]));

        var jwtToken = new JwtSecurityToken(
            issuer: _configuration["JwtConfiguration:JwtIssuer"],
            audience: _configuration["JwtConfiguration:JwtIssuer"],
            claims: claims,
            notBefore: DateTime.UtcNow,
            expires: DateTime.UtcNow.AddMinutes(int.Parse(_configuration["JwtConfiguration:JwtExpireMins"])),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
        );

        return new JwtSecurityTokenHandler().WriteToken(jwtToken);
    }

    public string GenerateRefreshToken(ClientType clientType)
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            var token = Convert.ToBase64String(randomNumber);

            var refreshToken = JsonConvert.SerializeObject(new RefreshToken(token, _jtiClaim.Value, clientType));

            _memoryCache.Set(_jtiClaim.Value, refreshToken, new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromDays(7)));
            return token;
        }
    }

    public RefreshToken GetRefreshToken(string jtiKey)
    {
        if (!_memoryCache.TryGetValue(jtiKey, out string refreshToken)) return null;
        _memoryCache.Remove(jtiKey);
        return JsonConvert.DeserializeObject<RefreshToken>(refreshToken);
    }

    public ClaimsPrincipal GetPrincipalFromExpiredToken(string accessToken)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false,
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtConfiguration:JwtKey"])),
            ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
        };

        var tokenHandler = new JwtSecurityTokenHandler();
        var principal = tokenHandler.ValidateToken(accessToken, tokenValidationParameters, out var securityToken);
        if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");

        return principal;
    }

AuthController.cs

        private readonly SignInManager<User> _signInManager;
    private readonly UserManager<User> _userManager;
    private readonly AuthService _authService;
    private readonly IMemoryCache _memoryCache;
    private readonly DataContext _context;

    public AuthController(UserManager<User> userManager, AuthService authService,
        SignInManager<User> signInManager, DataContext context)
    {
        _userManager = userManager;
        _authService = authService;
        _signInManager = signInManager;
        _context = context;
    }

    [HttpPost]
    public async Task<ActionResult> Login([FromBody] LoginDto model)
    {
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, false, false);

        if (!result.Succeeded) return BadRequest(new { isSucceeded = result.Succeeded, errors= "INVALID_LOGIN_ATTEMPT" });

        var appUser = _userManager.Users.Single(r => r.Email == model.Email);
        return Ok(new
        {
            isSucceeded = result.Succeeded,
            accessToken = _authService.GenerateAccessToken(GetClaims(appUser)),
            refreshToken = _authService.GenerateRefreshToken(model.ClientType)
        });
    }
    [HttpPost]
    public ActionResult RefreshToken([FromBody] RefreshTokenDto model)
    {
        var principal = _authService.GetPrincipalFromExpiredToken(model.AccessToken);
        var jtiKey = principal.Claims.Single(a => a.Type == JwtRegisteredClaimNames.Jti).Value;
        var refreshToken = _authService.GetRefreshToken(jtiKey);
        if (refreshToken == null)
            return BadRequest("Expired Refresh Token");
        if (refreshToken.Token != model.RefreshToken)
            return BadRequest("Invalid Refresh Token");
        return Ok(new
        {
            isSucceeded = true,
            accessToken = _authService.GenerateAccessToken(principal.Claims.SkipLast(1).ToList()),
            refreshToken = _authService.GenerateRefreshToken(model.ClientType)
        });

    }

我不确定这是刷新令牌的良好实现,因为刷新令牌可能在客户端应用程序中受到损害。

你能建议我一个更好的解决方案吗?

【问题讨论】:

    标签: asp.net-web-api .net-core jwt memorycache refresh-token


    【解决方案1】:

    如果涉及到安全性,那么性能就不那么重要了。但是对于一个长期存在的刷新令牌,数据库被命中的次数是可以忽略不计的。

    内存缓存不是存储刷新令牌的地方。在关闭的情况下,所有刷新令牌都将失效。所以无论如何你都需要持久化令牌。

    策略可以是一次只允许一个刷新令牌(保留在数据库中),并且在登录或刷新时用新令牌替换刷新令牌,这将使使用的刷新令牌无效。

    为了让事情更安全,您可以做的一件事是为刷新令牌使用固定的过期时间。在这种情况下,您将强制用户在固定时间后登录。限制令牌可能被破坏的窗口。

    另一种方法是减少令牌的寿命并使用滑动过期,这意味着每次使用刷新令牌时,都会重置过期。在这种情况下,用户可能永远不必再次登录,而在刷新时您可以进行一些检查。

    同时要求访问令牌和刷新令牌并不能让事情变得更安全。因为访问令牌可能已经过期(并且被泄露)并且可以存在多个访问令牌。请求新的访问令牌不会使当前令牌失效,并且您不想在每次调用时验证访问令牌。

    您不能简单地信任自己的令牌。您需要定义规则来检测对任一令牌的可疑使用。比如检查每分钟的通话次数,或者类似的东西。

    或者你可以检查当前的IP地址。为此,包括 IP 地址作为声明。如果当前 ip 地址与访问令牌中的 ip 地址不匹配,则拒绝访问以强制客户端刷新访问令牌。

    刷新时,如果 IP 地址未知(不在此用户的已知 IP 地址列表中),则用户需要登录。如果成功,您可以将 IP 地址添加到已验证的 IP 地址列表中。并且您可以向用户发送一封邮件,说明从另一个 IP 地址登录。

    您可以使用内存缓存来检测对访问令牌的可疑使用。在这种情况下,您可以撤销刷新令牌(只需将其从数据库中删除),让用户再次登录。

    【讨论】:

    • 感谢您的出色回答。我将在生产中使用 Redis(为了简单起见,我在这个问题中使用了 memoryCache),因为据我所知,它能够在一定程度上持久化数据,我认为它对于这种情况已经足够好了。我希望用户可以使用不同的刷新令牌登录多个设备,所以我认为策略解决方案不适合这种情况。我将为刷新令牌使用固定时间(在这种情况下为 7 天),并且在这种情况下我可能使用滑动(在 redis 或 memorycache 中)。我认为保存 IP 甚至在 db (redis) 更改时删除相关的刷新令牌很好。
    猜你喜欢
    • 1970-01-01
    • 2021-07-11
    • 1970-01-01
    • 2019-08-05
    • 2014-10-27
    • 1970-01-01
    • 1970-01-01
    • 2020-01-17
    • 2020-10-13
    相关资源
    最近更新 更多