Skip to content

Malomalsky-coder/Custom-authorization-based-on-API-keys-in-ASP.NET-Core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Кастомная авторизация на основе API ключей в ASP.NET Core

  1. Модель данных
public class ApiClient
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string ApiKey { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public bool IsActive { get; set; } = true;
}
  1. Сервис для работы с API ключами
public interface IApiKeyService
{
    Task<ApiClient?> GetClientByIdAsync(Guid id);
    Task<bool> ValidateTokenAsync(string token, Guid clientId, long timestamp);
    string GenerateApiKey();
}

public class ApiKeyService : IApiKeyService
{
    private readonly ApplicationDbContext _context;

    public ApiKeyService(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<ApiClient?> GetClientByIdAsync(Guid id)
    {
        return await _context.ApiClients
            .FirstOrDefaultAsync(c => c.Id == id && c.IsActive);
    }

    public async Task<bool> ValidateTokenAsync(string token, Guid clientId, long timestamp)
    {
        // Проверяем временную метку (например, ±5 минут)
        var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime;
        var currentTime = DateTime.UtcNow;
        
        if (Math.Abs((currentTime - requestTime).TotalMinutes) > 5)
            return false;

        // Получаем клиента
        var client = await GetClientByIdAsync(clientId);
        if (client == null) return false;

        // Формируем ожидаемый токен
        var expectedToken = GenerateHash($"{client.ApiKey}:{clientId}:{timestamp}");
        
        return string.Equals(token, expectedToken, StringComparison.OrdinalIgnoreCase);
    }

    public string GenerateApiKey()
    {
        var key = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
        return GenerateHash(key);
    }

    private static string GenerateHash(string input)
    {
        using var sha256 = SHA256.Create();
        var bytes = Encoding.UTF8.GetBytes(input);
        var hash = sha256.ComputeHash(bytes);
        return Convert.ToHexString(hash).ToLower();
    }
}
  1. Кастомный AuthenticationHandler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private readonly IApiKeyService _apiKeyService;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        IApiKeyService apiKeyService)
        : base(options, logger, encoder)
    {
        _apiKeyService = apiKeyService;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Пытаемся получить токен из заголовка
        if (!Request.Headers.TryGetValue("X-API-Key", out var apiKeyHeader) ||
            !Request.Headers.TryGetValue("X-Client-Id", out var clientIdHeader) ||
            !Request.Headers.TryGetValue("X-Timestamp", out var timestampHeader))
        {
            return AuthenticateResult.Fail("Missing API authentication headers");
        }

        var token = apiKeyHeader.ToString();
        if (!Guid.TryParse(clientIdHeader, out var clientId))
            return AuthenticateResult.Fail("Invalid client ID format");

        if (!long.TryParse(timestampHeader, out var timestamp))
            return AuthenticateResult.Fail("Invalid Timestamp format");

        // Валидируем токен
        var isValid = await _apiKeyService.ValidateTokenAsync(token, clientId, timestamp);
        if (!isValid)
            return AuthenticateResult.Fail("Invalid API token");

        // Получаем клиента
        var client = await _apiKeyService.GetClientByIdAsync(clientId);
        if (client == null)
            return AuthenticateResult.Fail("Client not found");

        // Создаем claims
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, client.Id.ToString()),
            new Claim(ClaimTypes.Name, client.Name),
            new Claim("ClientType", "ApiClient")
        };

        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}

public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "ApiKey";
}
  1. Кастомный AuthorizationHandler
public class ClientAuthorizationHandler : AuthorizationHandler<ClientRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ClientRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "ClientType" && c.Value == "ApiClient"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

public class ClientRequirement : IAuthorizationRequirement
{
}
  1. Контроллер для управления клиентами
[ApiController]
[Route("api/[controller]")]
public class ClientsController : ControllerBase
{
    private readonly IApiKeyService _apiKeyService;
    private readonly ApplicationDbContext _context;

    public ClientsController(IApiKeyService apiKeyService, ApplicationDbContext context)
    {
        _apiKeyService = apiKeyService;
        _context = context;
    }

    [HttpPost]
    [Authorize] // Стандартная авторизация для админов
    public async Task<IActionResult> CreateClient([FromBody] CreateClientRequest request)
    {
        var client = new ApiClient
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            ApiKey = _apiKeyService.GenerateApiKey(),
            CreatedAt = DateTime.UtcNow,
            IsActive = true
        };

        _context.ApiClients.Add(client);
        await _context.SaveChangesAsync();

        // Передаем клиенту
        return Ok(new
        {
            ClientId = client.Id,
            ApiKey = client.ApiKey 
        });
    }
}

public class CreateClientRequest
{
    public string Name { get; set; } = string.Empty;
}
  1. Настройка в Program.cs
var builder = WebApplication.CreateBuilder(args);

// База данных
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Стандартная аутентификация (JWT или другая)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // Настройки JWT
    });

// Кастомная аутентификация API ключей
builder.Services.AddAuthentication()
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(
        ApiKeyAuthenticationOptions.DefaultScheme, 
        options => { });

// Авторизация
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Client", policy =>
        policy.Requirements.Add(new ClientRequirement()));
});

// Сервисы
builder.Services.AddScoped<IApiKeyService, ApiKeyService>();
builder.Services.AddScoped<IAuthorizationHandler, ClientAuthorizationHandler>();
builder.Services.AddControllers();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
  1. Использование в контроллерах
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
    [HttpGet("client-data")]
    [Authorize(Policy = "Client")] // Только для API клиентов
    public IActionResult GetClientData()
    {
        var clientId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var clientName = User.FindFirst(ClaimTypes.Name)?.Value;
        
        return Ok(new { message = $"Hello {clientName} ({clientId})" });
    }

    [HttpGet("user-data")]
    [Authorize] // Стандартная авторизация
    public IActionResult GetUserData()
    {
        return Ok(new { message = "Hello regular user" });
    }

    [HttpGet("mixed-data")]
    [Authorize] // Любой авторизованный пользователь (и стандартный, и API клиент)
    public IActionResult GetMixedData()
    {
        var isApiClient = User.HasClaim("ClientType", "ApiClient");
        
        return Ok(new { 
            message = "Hello authenticated entity",
            isApiClient = isApiClient
        });
    }
}
  1. Пример запроса от клиента
GET /api/secure/client-data HTTP/1.1
Host: yourapi.com
X-API-Key: d49deb22c6e43bfdd9e161d5f1d4710e4300570845a4fe16e0263140e2a7c510
X-Server-Id: 12345678-1234-1234-1234-123456789abc
X-Timestamp: 1759091560

About

Реализация кастомной авторизации на основе API ключей в ASP .NET Core

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published