Как реализовать JWT Обновить токены в asp.net core web api (нет стороннего участника)?

Я занимаюсь внедрением web-api с использованием ядра asp.net, использующего JWT. Я не использую стороннее решение, такое как IdentityServer4, как я пытаюсь узнать.

Я получил конфигурацию JWT, но не понимаю, как реализовать токены обновления, когда истечет срок действия JWT.

Ниже приведен пример кода в моем методе Configure внутри startup.cs.

app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
    AuthenticationScheme = "Jwt",
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = new TokenValidationParameters()
    {
        ValidAudience = Configuration["Tokens:Audience"],
        ValidIssuer = Configuration["Tokens:Issuer"],
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"])),
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    }
});

Ниже приведен метод контроллера, используемый для генерации JWT. Я тестировал срок до 30 секунд.

    [Route("Token")]
    [HttpPost]
    public async Task<IActionResult> CreateToken([FromBody] CredentialViewModel model)
    {
        try
        {
            var user = await _userManager.FindByNameAsync(model.Username);

            if (user != null)
            {
                if (_hasher.VerifyHashedPassword(user, user.PasswordHash, model.Password) == PasswordVerificationResult.Success)
                {
                    var userClaims = await _userManager.GetClaimsAsync(user);

                    var claims = new[]
                    {
                        new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
                    }.Union(userClaims);

                    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwt.Key));
                    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

                    var token = new JwtSecurityToken(
                            issuer: _jwt.Issuer,
                            audience: _jwt.Audience,
                            claims: claims,
                            expires: DateTime.UtcNow.AddSeconds(30),
                            signingCredentials: creds
                        );

                    return Ok(new
                    {
                        access_token = new JwtSecurityTokenHandler().WriteToken(token),
                        expiration = token.ValidTo
                    });
                }
            }
        }
        catch (Exception)
        {

        }

        return BadRequest("Failed to generate token.");
    }

Был бы очень благодарен за некоторые рекомендации.

Ответ 1

Прежде всего, вам нужно сгенерировать токен обновления и сохранить его где-нибудь. Это потому, что вы хотите иметь возможность сделать его недействительным, если это необходимо. Если вы должны следовать тому же шаблону, что и токен доступа - где все данные содержатся в токене - токен, который попадает в чужие руки, может быть использован для генерации новых токенов доступа на весь срок действия токена обновления, который может быть очень долго

Так какого черта ты должен сохраняться?

Вам нужен какой-то уникальный идентификатор, который не легко угадать, GUID подойдет просто отлично. Вам также нужны данные, чтобы иметь возможность выдать новый токен доступа, скорее всего, имя пользователя. Имея имя пользователя, вы можете пропустить VerifyHashedPassword (...) -part, но в остальном просто следуйте той же логике.

Чтобы получить токен обновления, вы обычно используете область "offline_access", которую вы предоставляете в своей модели (CredentialViewModel) при выполнении запроса токена. В отличие от обычного запроса токена доступа, вам не нужно указывать имя пользователя и пароль, а вместо этого токен обновления. При получении запроса с токеном обновления вы просматриваете постоянный идентификатор и выдает токен для найденного пользователя.

Ниже приведен псевдокод для счастливого пути:

[Route("Token")]
[HttpPost]
public async Task<IActionResult> CreateToken([FromBody] CredentialViewModel model)
{
    if (model.GrantType is "refresh_token")
    {
        // Lookup which user is tied to model.RefreshToken
        // Generate access token from the username (no password check required)
        // Return the token (access + expiration)
    }
    else if (model.GrantType is "password")
    {
        if (model.Scopes contains "offline_access")
        {
            // Generate access token
            // Generate refresh token (random GUID + model.username)
            // Persist refresh token
            // Return the complete token (access + refresh + expiration)
        }
        else
        {
            // Generate access token
            // Return the token (access + expiration)
        }
    }
}