Поговорим о реализации аутентификации в веб-сервисе, построенном с использованием Django Rest Framework (DRF). Мы столкнулись с вызовом при поддержке двух методов аутентификации: токены DRF и OpenID Connect (OIDC) из Keycloak. Пересматривать всю логику авторизации и внедрять ее с нуля было непрактично в свете принципов SOLID. В данном контексте мы посмотрим на сохранение логики проверки токенов в базовых классах и использование Middleware для поддержки аутентификации через Websocket.
Проблема
Существующий сервис, разработанный на основе DRF, предоставлял два метода аутентификации: токены DRF и OpenID Connect (OIDC) из Keycloak. Возможность пересмотра логики с нуля была исключена в связи с принципами SOLID. Наша задача заключалась в сохранении основных классов и добавлении поддержки нового метода аутентификации через Websocket.
Решение
Процесс реализации сводился к внедрению Middleware, который расширял текущую логику аутентификации в DRF. Давайте разберемся с этим пошагово.
Определение роутов
Начнем с определения роутов, которые нужно обернуть в созданный Middleware для обеспечения аутентификации через Websocket.
websocket_urlpatterns = TelebackAuthMiddlewareStack(
URLRouter([
re_path(r"^notifications$", consumers.NotificationsConsumer.as_asgi(), name="ws_notifications", ),
])
)
Создание Middleware
Новый уровень аутентификации был добавлен через создание Middleware.
class AsyncTokenAuthentication(TokenAuthentication):
async def authenticate(self, request):
return await sync_to_async(super().authenticate)(request)
class AsyncOIDCAuthentication(OIDCAuthentication):
async def authenticate(self, request, **kwargs):
return await sync_to_async(super().authenticate)(request)
def make_request_of_scope(scope):
request = HttpRequest()
headers = scope.get('headers', [])
auth_key = b'authorization'
for name, value in headers:
if name == auth_key:
request.META["HTTP_AUTHORIZATION"] = value
return request
class MakeRequestMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope['request'] = make_request_of_scope(scope)
return await self.app(scope, receive, send)
class TelebackOIDCAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
try:
user, _ = await AsyncOIDCAuthentication().authenticate(scope['request'])
scope['user'] = user
except (Exception,):
pass
return await self.app(scope, receive, send)
class TokenAuthMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
try:
user, _ = await AsyncTokenAuthentication().authenticate(scope['request'])
scope['user'] = user
except (Exception,):
pass
return await self.app(scope, receive, send)
def TelebackAuthMiddlewareStack(inner):
return MakeRequestMiddleware(TokenAuthMiddleware(TelebackOIDCAuthMiddleware(inner)))
В нашем веб-сервисе, построенном на библиотеке Channels (https://channels.readthedocs.io/en/latest/) с использованием ASGI, нам потребовалось асинхронное выполнение методов аутентификации. Особое внимание уделяется асинхронному подходу при проверке токена через AsyncTokenAuthentication и AsyncOIDCAuthentication. В этом контексте рассмотрим оптимизацию функции make_request_of_scope, которая теперь создает HttpRequest асинхронно и добавляет заголовок для аутентификации. Мы также внедрили отдельный посредник MakeRequestMiddleware для повторного использования request. В заключении, аутентификацию вызываем через TelebackOIDCAuthMiddleware и TokenAuthMiddleware, сохраняя результат в scope для будущего использования.