providers/oauth2: don't create events before client_id can be verified to prevent spam

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-05-14 00:02:01 +02:00
parent 7d41e6227b
commit 8ba45a5f6a
2 changed files with 114 additions and 114 deletions

View File

@ -24,7 +24,7 @@ class OAuth2Error(SentryIgnoredException):
return self.error return self.error
def to_event(self, message: Optional[str] = None, **kwargs) -> Event: def to_event(self, message: Optional[str] = None, **kwargs) -> Event:
"""Create configuration_error Event and save it.""" """Create configuration_error Event."""
return Event.new( return Event.new(
EventAction.CONFIGURATION_ERROR, EventAction.CONFIGURATION_ERROR,
message=message or self.description, message=message or self.description,

View File

@ -253,6 +253,119 @@ class OAuthAuthorizationParams:
return code return code
class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams
def pre_permission_check(self):
"""Check prompt parameter before checking permission/authentication,
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
# Quick sanity check at the beginning to prevent event spamming
if len(self.request.GET) < 1:
raise Http404
try:
self.params = OAuthAuthorizationParams.from_request(self.request)
except AuthorizeError as error:
LOGGER.warning(error.description, redirect_uri=error.redirect_uri)
raise RequestValidationError(HttpResponseRedirect(error.create_uri()))
except OAuth2Error as error:
LOGGER.warning(error.description)
raise RequestValidationError(
bad_request_message(self.request, error.description, title=error.error)
)
except OAuth2Provider.DoesNotExist:
raise Http404
if PROMPT_NONE in self.params.prompt and not self.request.user.is_authenticated:
# When "prompt" is set to "none" but the user is not logged in, show an error message
error = AuthorizeError(
self.params.redirect_uri,
"login_required",
self.params.grant_type,
self.params.state,
)
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
raise RequestValidationError(HttpResponseRedirect(error.create_uri()))
def resolve_provider_application(self):
client_id = self.request.GET.get("client_id")
self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
self.application = self.provider.application
def modify_policy_request(self, request: PolicyRequest) -> PolicyRequest:
request.context["oauth_scopes"] = self.params.scope
request.context["oauth_grant_type"] = self.params.grant_type
request.context["oauth_code_challenge"] = self.params.code_challenge
request.context["oauth_code_challenge_method"] = self.params.code_challenge_method
request.context["oauth_max_age"] = self.params.max_age
request.context["oauth_redirect_uri"] = self.params.redirect_uri
request.context["oauth_response_type"] = self.params.response_type
return request
# pylint: disable=unused-argument
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell"""
# After we've checked permissions, and the user has access, check if we need
# to re-authenticate the user
if self.params.max_age:
current_age: timedelta = (
timezone.now()
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user))
.latest("created")
.created
)
if current_age.total_seconds() > self.params.max_age:
return self.handle_no_permission()
# If prompt=login, we need to re-authenticate the user regardless
if (
PROMPT_LOGIN in self.params.prompt
and SESSION_NEEDS_LOGIN not in self.request.session
# To prevent the user from having to double login when prompt is set to login
# and the user has just signed it. This session variable is set in the UserLoginStage
# and is (quite hackily) removed from the session in applications's API's List method
and USER_LOGIN_AUTHENTICATED not in self.request.session
):
self.request.session[SESSION_NEEDS_LOGIN] = True
return self.handle_no_permission()
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
# planner.use_cache = False
planner.allow_empty_flows = True
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
plan: FlowPlan = planner.plan(
self.request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_PARAMS: self.params,
# 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,
},
)
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
# need to inject a consent stage
if PROMPT_CONSENT in self.params.prompt:
if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings):
# Plan does not have any consent stage, so we add an in-memory one
stage = ConsentStage(
name="OAuth2 Provider In-memory consent stage",
mode=ConsentMode.ALWAYS_REQUIRE,
)
plan.append_stage(stage)
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
class OAuthFulfillmentStage(StageView): class OAuthFulfillmentStage(StageView):
"""Final stage, restores params from Flow.""" """Final stage, restores params from Flow."""
@ -439,116 +552,3 @@ class OAuthFulfillmentStage(StageView):
query_fragment["state"] = self.params.state if self.params.state else "" query_fragment["state"] = self.params.state if self.params.state else ""
return query_fragment return query_fragment
class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams
def pre_permission_check(self):
"""Check prompt parameter before checking permission/authentication,
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
# Quick sanity check at the beginning to prevent event spamming
if len(self.request.GET) < 1:
raise Http404
try:
self.params = OAuthAuthorizationParams.from_request(self.request)
except AuthorizeError as error:
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
raise RequestValidationError(HttpResponseRedirect(error.create_uri()))
except OAuth2Error as error:
error.to_event().from_http(self.request)
raise RequestValidationError(
bad_request_message(self.request, error.description, title=error.error)
)
except OAuth2Provider.DoesNotExist:
raise Http404
if PROMPT_NONE in self.params.prompt and not self.request.user.is_authenticated:
# When "prompt" is set to "none" but the user is not logged in, show an error message
error = AuthorizeError(
self.params.redirect_uri,
"login_required",
self.params.grant_type,
self.params.state,
)
error.to_event(redirect_uri=error.redirect_uri).from_http(self.request)
raise RequestValidationError(HttpResponseRedirect(error.create_uri()))
def resolve_provider_application(self):
client_id = self.request.GET.get("client_id")
self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
self.application = self.provider.application
def modify_policy_request(self, request: PolicyRequest) -> PolicyRequest:
request.context["oauth_scopes"] = self.params.scope
request.context["oauth_grant_type"] = self.params.grant_type
request.context["oauth_code_challenge"] = self.params.code_challenge
request.context["oauth_code_challenge_method"] = self.params.code_challenge_method
request.context["oauth_max_age"] = self.params.max_age
request.context["oauth_redirect_uri"] = self.params.redirect_uri
request.context["oauth_response_type"] = self.params.response_type
return request
# pylint: disable=unused-argument
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell"""
# After we've checked permissions, and the user has access, check if we need
# to re-authenticate the user
if self.params.max_age:
current_age: timedelta = (
timezone.now()
- Event.objects.filter(action=EventAction.LOGIN, user=get_user(self.request.user))
.latest("created")
.created
)
if current_age.total_seconds() > self.params.max_age:
return self.handle_no_permission()
# If prompt=login, we need to re-authenticate the user regardless
if (
PROMPT_LOGIN in self.params.prompt
and SESSION_NEEDS_LOGIN not in self.request.session
# To prevent the user from having to double login when prompt is set to login
# and the user has just signed it. This session variable is set in the UserLoginStage
# and is (quite hackily) removed from the session in applications's API's List method
and USER_LOGIN_AUTHENTICATED not in self.request.session
):
self.request.session[SESSION_NEEDS_LOGIN] = True
return self.handle_no_permission()
# Regardless, we start the planner and return to it
planner = FlowPlanner(self.provider.authorization_flow)
# planner.use_cache = False
planner.allow_empty_flows = True
scope_descriptions = UserInfoView().get_scope_descriptions(self.params.scope)
plan: FlowPlan = planner.plan(
self.request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_PARAMS: self.params,
# 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,
},
)
# OpenID clients can specify a `prompt` parameter, and if its set to consent we
# need to inject a consent stage
if PROMPT_CONSENT in self.params.prompt:
if not any(isinstance(x.stage, ConsentStageView) for x in plan.bindings):
# Plan does not have any consent stage, so we add an in-memory one
stage = ConsentStage(
name="OAuth2 Provider In-memory consent stage",
mode=ConsentMode.ALWAYS_REQUIRE,
)
plan.append_stage(stage)
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.provider.authorization_flow.slug,
)