Современные веб‑приложения всё чаще работают в виде микросервисов и открытых API. Защита таких точек входа требует надёжного механизма аутентификации и авторизации. Наиболее популярным решением является комбинация OAuth 2.0 и OpenID Connect (OIDC), реализованная через Identity Provider – в нашем случае Keycloak. В статье рассматривается, как быстро и безопасно интегрировать эту схему в два популярных стека: Laravel (PHP) и FastAPI (Python).
OAuth 2.0 и OpenID Connect: основные понятия
OAuth 2.0 – протокол делегирования доступа, позволяющий клиенту получать токен доступа (access_token) от авторизационного сервера без передачи пользовательских учётных данных. OpenID Connect – надстройка над OAuth 2.0, добавляющая слой аутентификации: в токен id_token включается информация о пользователе (claims).
- Authorization Code Flow – лучший выбор для серверных приложений и SPA.
- Client Credentials Flow – используется для машинного взаимодействия (service‑to‑service).
- Refresh Token – позволяет обновлять
access_tokenбез повторного логина.
Keycloak поддерживает все стандартные потоки, а также предоставляет UI для управления клиентами, ролями и политиками доступа.
Keycloak как централизованный провайдер идентификации
Перед тем как приступить к интеграции, необходимо подготовить Keycloak:
- Создать Realm – изолированную область безопасности.
- Внутри Realm добавить два Client:
laravel-api– типconfidential, включитьStandard FlowиDirect Access Grants.fastapi-service– типbearer-only(если FastAPI будет только проверять токены) либоconfidentialдля собственного токен‑выпуска.
- Определить нужные Roles и привязать их к клиентам.
- Сгенерировать client secret и сохранить его в безопасном месте.
Интеграция с Laravel
В Laravel есть два популярных подхода:
- Использовать
laravel/passport– полностью реализует OAuth 2.0, но требует собственного сервера токенов. - Воспользоваться
socialiteproviders/keycloak(или обычнымleague/oauth2-client) – Laravel остаётся клиентом Keycloak.
Ниже пример минимальной конфигурации через socialiteproviders/keycloak:
composer require socialiteproviders/keycloak
// config/services.php
return [
'keycloak' => [
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirect' => env('KEYCLOAK_REDIRECT_URI'),
'base_url' => env('KEYCLOAK_BASE_URL'), // https://keycloak.example.com/auth
'realm' => env('KEYCLOAK_REALM'),
],
];
Маршрут для редиректа:
// routes/web.php
Route::get('login/keycloak', function () {
return Socialite::driver('keycloak')->redirect();
});
Route::get('login/keycloak/callback', function () {
$user = Socialite::driver('keycloak')->user();
// Поиск/создание локального пользователя
$localUser = \App\Models\User::firstOrCreate([
'email' => $user->getEmail(),
], [
'name' => $user->getName(),
'keycloak_id' => $user->getId(),
]);
Auth::login($localUser);
return redirect('/home');
});
Для защиты API‑эндпоинтов создаём middleware, которое проверяет Bearer токен через публичный ключ Keycloak:
// app/Http/Middleware/KeycloakToken.php
namespace App\Http\Middleware;
use Closure;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class KeycloakToken
{
public function handle($request, Closure $next)
{
$authHeader = $request->header('Authorization');
if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
$token = $matches[1];
// Получаем публичный ключ из Keycloak (можно кэшировать)
$jwks = json_decode(file_get_contents(config('services.keycloak.base_url').'/realms/'.config('services.keycloak.realm').'/protocol/openid-connect/certs'));
foreach ($jwks->keys as $jwk) {
try {
$payload = JWT::decode($token, new Key($jwk->x5c[0], $jwk->alg));
// При необходимости проверяем роли
$request->attributes->set('jwt_payload', (array) $payload);
return $next($request);
} catch (\Exception $e) {
// пробуем следующий ключ
}
}
return response()->json(['error' => 'Invalid token'], 401);
}
}
Регистрация middleware в app/Http/Kernel.php и применение к роутам:
protected $routeMiddleware = [
// ... другие middleware
'keycloak.auth' => \App\Http\Middleware\KeycloakToken::class,
];
// routes/api.php
Route::middleware('keycloak.auth')->group(function () {
Route::get('/user/profile', [UserController::class, 'profile']);
});
Интеграция с FastAPI
Для Python‑стека удобно использовать python-keycloak (клиент) совместно с Authlib или готовый пакет fastapi-keycloak. Ниже показан пример без внешних зависимостей – только python-jose для валидации JWT.
# Установка зависимостей
pip install fastapi uvicorn python-jose[cryptography] httpx
Файл keycloak_auth.py – утилита для получения JWK и проверки токена:
import httpx
from jose import jwt, JWTError
from functools import lru_cache
KEYCLOAK_URL = "https://keycloak.example.com/auth"
REALM = "myrealm"
@lru_cache()
def get_jwks():
resp = httpx.get(f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/certs")
resp.raise_for_status()
return resp.json()["keys"]
def verify_token(token: str):
jwks = get_jwks()
for jwk in jwks:
try:
payload = jwt.decode(
token,
jwk,
algorithms=[jwk["alg"]],
audience="fastapi-service",
issuer=f"{KEYCLOAK_URL}/realms/{REALM}"
)
return payload
except JWTError:
continue
raise JWTError("Invalid token")
Middleware в FastAPI, которое добавляет payload в запрос:
from fastapi import FastAPI, Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI()
class KeycloakMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
auth = request.headers.get('Authorization')
if not auth or not auth.lower().startswith('bearer '):
raise HTTPException(status_code=401, detail='Unauthenticated')
token = auth.split(' ', 1)[1]
try:
payload = verify_token(token)
request.state.user = payload
except JWTError as e:
raise HTTPException(status_code=401, detail='Invalid token')
response = await call_next(request)
return response
app.add_middleware(KeycloakMiddleware)
@app.get('/protected')
async def protected_endpoint(request: Request):
return {"user": request.state.user}
Для сервис‑to‑service взаимодействия (Client Credentials Flow) достаточно запросить токен через httpx:
async def get_service_token():
data = {
'grant_type': 'client_credentials',
'client_id': 'fastapi-service',
'client_secret': 'YOUR_CLIENT_SECRET',
}
resp = await httpx.post(
f"{KEYCLOAK_URL}/realms/{REALM}/protocol/openid-connect/token",
data=data,
)
resp.raise_for_status()
return resp.json()['access_token']
Практические рекомендации
- Храните
client_secretв безопасных переменных окружения (Docker secrets, .env файлы с ограниченным доступом). - Кешируйте JWK‑ключи (например, на 5‑10 минут) – уменьшит нагрузку на Keycloak.
- Включайте проверку
exp,nbfиaudв каждом сервисе. - Для микросервисов используйте Client Credentials Flow и отдельные клиентские роли.
- Регулярно обновляйте зависимости (особенно библиотеки JWT) для защиты от уязвимостей.
- Логи безопасности: фиксируйте неудачные попытки аутентификации и истёкшие токены.
Заключение
Интеграция OAuth 2.0 и OpenID Connect через Keycloak предоставляет единый, масштабируемый и проверенный способ защиты API как в PHP‑стеке (Laravel), так и в Python‑стеке (FastAPI). Правильная настройка клиента, валидные токены и соблюдение best‑practice позволяют построить надёжную архитектуру, готовую к росту и соответствию требованиям GDPR, PCI‑DSS и другим нормативам.
Услуги RybinskLab
RybinskLab предлагает полный цикл разработки защищённых API: от архитектурного проектирования и настройки Keycloak до внедрения кастомных middleware в Laravel и FastAPI. Мы поможем ускорить вывод продукта на рынок, обеспечив при этом высокий уровень безопасности и соответствие отраслевым стандартам. Свяжитесь с нами, чтобы обсудить ваш проект.