Как была реализована аутентификация в API на вашем проекте?

«Как была реализована аутентификация в API на вашем проекте?» — вопрос из категории Безопасность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В проекте использовалась аутентификация на основе JWT (JSON Web Tokens) по схеме Bearer Token в связке с Refresh Tokens для баланса безопасности и удобства пользователя.

Архитектура потока

  1. Логин: Клиент отправляет POST /api/auth/login с учетными данными.
  2. Верификация: Сервер проверяет логин/пароль (обычно против хэша в БД).
  3. Генерация токенов:
    • Access Token (JWT): Короткоживущий (15-30 мин), содержит claims (userId, roles, permissions). Подписывается секретным ключом.
    • Refresh Token: Долгоживущий (7-30 дней), хранится в БД в связке с userId, IP-адресом (опционально) и может быть отозван.
  4. Ответ: Сервер возвращает оба токена клиенту.
  5. Доступ к API: Клиент прикладывает Access Token в заголовке Authorization: Bearer <token> к каждому запросу.
  6. Обновление: По истечении срока Access Token, клиент использует Refresh Token для вызова POST /api/auth/refresh и получения новой пары токенов.

Реализация в ASP.NET Core

Конфигурация сервисов (Program.cs / Startup.cs):

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = Configuration["Jwt:Audience"],
            ValidateLifetime = true, // Критически важно!
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])
            ),
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.Zero // Убираем запас времени для точной проверки срока.
        };
        // Для аутентификации из WebSocket или SignalR соединений
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                // Извлекаем токен из query string для WebSockets
                if (!string.IsNullOrEmpty(accessToken))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy =>
        policy.RequireClaim("role", "Admin"));
});

Генерация токена (пример):

public string GenerateAccessToken(User user)
{
    var claims = new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.Email, user.Email),
        new Claim("role", user.Role),
        new Claim("custom_claim", "some_value")
    };

    var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

    var token = new JwtSecurityToken(
        issuer: _config["Jwt:Issuer"],
        audience: _config["Jwt:Audience"],
        claims: claims,
        expires: DateTime.UtcNow.AddMinutes(30), // Короткая жизнь
        signingCredentials: creds);

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

Защита endpoint'ов:

[ApiController]
[Route("api/[controller]")]
[Authorize] // Все методы контроллера требуют аутентификации
public class SecureController : ControllerBase
{
    [HttpGet("admin-data")]
    [Authorize(Policy = "RequireAdminRole")] // Дополнительная проверка роли
    public IActionResult GetAdminData() { ... }

    [HttpGet("profile")]
    public IActionResult GetProfile()
    {
        // Получение ID текущего пользователя из токена
        var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
        ...
    }
}

Дополнительные меры безопасности

  • HTTPS: Обязательное использование для всего трафика.
  • Хранение на клиенте: Access Token хранится в памяти (переменная JS), а не в localStorage, чтобы избежать XSS-атак. Refresh Token хранится в HttpOnly cookie для защиты от XSS.
  • Отзыв токенов: При смене пароля или выходе (logout) Refresh Token удаляется/инвалидируется на сервере.
  • Валидация: Проверка срока действия, подписи, издателя и аудитории.