Аутентифікація за допомогою API-ключа для веб-додатків
API-ключі — найпростіший механізм аутентифікації для взаємодії машина-машина (server-to-server). Без OAuth-flow, без refresh-токенів, без сесій. Клієнт передає ключ у заголовку або параметрі запиту, сервер його перевіряє. Підходить для публічних API, інтеграцій з партнерами та інструментів CLI.
Генерування та зберігання ключів
Ключ повинен бути досить випадковим — мінімум 32 байти:
// Генерування ключа
$key = 'sk_' . bin2hex(random_bytes(32)); // sk_ + 64 hex = 67 символів
// Приклад: sk_a3f9b12e8c4d7e1f0a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5
// Ніколи не зберігаємо ключ у відкритому вигляді — тільки хеш
$hash = hash('sha256', $key);
DB::table('api_keys')->insert([
'user_id' => $userId,
'name' => $request->name,
'key_prefix' => substr($key, 0, 8), // для відображення користувачу
'key_hash' => $hash,
'scopes' => json_encode(['read:articles', 'write:articles']),
'last_used_at' => null,
'expires_at' => now()->addYear(),
]);
// Ключ показуємо користувачу ОДИН РАЗ — при створенні
return response()->json(['key' => $key], 201);
Зберігаємо тільки хеш — при витоку бази ключі непридатні.
Перевірка ключа
// Middleware ApiKeyAuth
public function handle(Request $request, Closure $next): Response
{
$key = $request->bearerToken() // Authorization: Bearer sk_...
?? $request->header('X-Api-Key') // X-Api-Key: sk_...
?? $request->query('api_key'); // ?api_key=sk_... (уникати у URL)
if (!$key) {
return response()->json(['error' => 'API key required'], 401);
}
$hash = hash('sha256', $key);
$apiKey = ApiKey::where('key_hash', $hash)
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->first();
if (!$apiKey) {
return response()->json(['error' => 'Invalid or expired API key'], 401);
}
// Оновлюємо last_used_at асинхронно, щоб не сповільнювати запит
dispatch(fn() => $apiKey->update(['last_used_at' => now()]))->afterResponse();
$request->setUserResolver(fn() => $apiKey->user);
$request->attributes->set('api_key', $apiKey);
return $next($request);
}
Scopes (дозволи)
Ключ повинен мати мінімально необхідні права:
// Перевірка scope у контролері або Middleware
public function store(Request $request): JsonResponse
{
$apiKey = $request->attributes->get('api_key');
if (!in_array('write:articles', $apiKey->scopes ?? [])) {
return response()->json(['error' => 'Insufficient scope'], 403);
}
// ...
}
Список scopes визначається під час створення ключа користувачем (або призначається фіксований набір).
Безпека
Передача ключа:
- Тільки через HTTPS
- Переважно у заголовку
Authorization: BearerабоX-Api-Key - Не у URL (потрапляє у логи, історію браузера, referer)
Ротація ключів:
// Інвалідуємо старий ключ при створенні нового
ApiKey::where('user_id', $userId)->where('name', $name)->delete();
Аудит використання:
ApiKeyUsageLog::create([
'api_key_id' => $apiKey->id,
'ip' => $request->ip(),
'endpoint' => $request->path(),
'method' => $request->method(),
'status' => null, // заповнюється у Terminate middleware
]);
Обмеження частоти за ключем:
RateLimiter::for('api-key', function (Request $request) {
$apiKey = $request->attributes->get('api_key');
return Limit::perMinute($apiKey->rate_limit ?? 60)->by($apiKey->id);
});
UX у панелі керування
Список ключів з відображенням key_prefix (перші 8 символів):
sk_a3f9b1... "Production integration" Останнє використання: 2 години тому [Видалити]
sk_c2d4e5... "Staging webhook" Ніколи не використовувався [Видалити]
Додати кнопку «Показати ключ» неможливо — він не зберігається. Тільки повторне створення.
Терміни
Таблиця api_keys, middleware перевірки, UI для створення/видалення ключів, обмеження частоти за ключем: 1–2 дні.







