security: fix CVE-2024-38371 (#10229)
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
"""Device flow views"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from structlog.stdlib import get_logger
|
||||
@ -16,7 +17,8 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||
from authentik.policies.views import PolicyAccessView
|
||||
from authentik.providers.oauth2.models import DeviceToken
|
||||
from authentik.providers.oauth2.views.device_finish import (
|
||||
PLAN_CONTEXT_DEVICE,
|
||||
OAuthDeviceCodeFinishStage,
|
||||
@ -31,60 +33,52 @@ LOGGER = get_logger()
|
||||
QS_KEY_CODE = "code" # nosec
|
||||
|
||||
|
||||
def get_application(provider: OAuth2Provider) -> Application | None:
|
||||
"""Get application from provider"""
|
||||
try:
|
||||
app = provider.application
|
||||
if not app:
|
||||
class CodeValidatorView(PolicyAccessView):
|
||||
"""Helper to validate frontside token"""
|
||||
|
||||
def __init__(self, code: str, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.code = code
|
||||
|
||||
def resolve_provider_application(self):
|
||||
self.token = DeviceToken.objects.filter(user_code=self.code).first()
|
||||
if not self.token:
|
||||
raise Application.DoesNotExist
|
||||
self.provider = self.token.provider
|
||||
self.application = self.token.provider.application
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs):
|
||||
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
|
||||
planner = FlowPlanner(self.provider.authorization_flow)
|
||||
planner.allow_empty_flows = True
|
||||
planner.use_cache = False
|
||||
try:
|
||||
plan = planner.plan(
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_APPLICATION: self.application,
|
||||
# OAuth2 related params
|
||||
PLAN_CONTEXT_DEVICE: self.token,
|
||||
# Consent related params
|
||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||
% {"application": self.application.name},
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||
},
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
LOGGER.warning("Flow not applicable to user")
|
||||
return None
|
||||
return app
|
||||
except Application.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
|
||||
"""Validate user token"""
|
||||
token = DeviceToken.objects.filter(
|
||||
user_code=code,
|
||||
).first()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
app = get_application(token.provider)
|
||||
if not app:
|
||||
return None
|
||||
|
||||
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
|
||||
planner = FlowPlanner(token.provider.authorization_flow)
|
||||
planner.allow_empty_flows = True
|
||||
planner.use_cache = False
|
||||
try:
|
||||
plan = planner.plan(
|
||||
request,
|
||||
{
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_APPLICATION: app,
|
||||
# OAuth2 related params
|
||||
PLAN_CONTEXT_DEVICE: token,
|
||||
# Consent related params
|
||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||
% {"application": app.name},
|
||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
|
||||
},
|
||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
request.GET,
|
||||
flow_slug=self.token.provider.authorization_flow.slug,
|
||||
)
|
||||
except FlowNonApplicableException:
|
||||
LOGGER.warning("Flow not applicable to user")
|
||||
return None
|
||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||
request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
request.GET,
|
||||
flow_slug=token.provider.authorization_flow.slug,
|
||||
)
|
||||
|
||||
|
||||
class DeviceEntryView(View):
|
||||
class DeviceEntryView(PolicyAccessView):
|
||||
"""View used to initiate the device-code flow, url entered by endusers"""
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
@ -94,7 +88,9 @@ class DeviceEntryView(View):
|
||||
LOGGER.info("Brand has no device code flow configured", brand=brand)
|
||||
return HttpResponse(status=404)
|
||||
if QS_KEY_CODE in request.GET:
|
||||
validation = validate_code(request.GET[QS_KEY_CODE], request)
|
||||
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
|
||||
request
|
||||
)
|
||||
if validation:
|
||||
return validation
|
||||
LOGGER.info("Got code from query parameter but no matching token found")
|
||||
@ -131,7 +127,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
|
||||
|
||||
def validate_code(self, code: int) -> HttpResponse | None:
|
||||
"""Validate code and save the returned http response"""
|
||||
response = validate_code(code, self.stage.request)
|
||||
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
|
||||
if not response:
|
||||
raise ValidationError(_("Invalid code"), "invalid")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user