From d4abf5621e129a0779d92670a580493787a9a8a4 Mon Sep 17 00:00:00 2001 From: scheibling <24367830+scheibling@users.noreply.github.com> Date: Thu, 12 May 2022 21:36:31 +0200 Subject: [PATCH] providers/oauth2: add support for form_post response mode (#2818) * Added request verification and parameter generation * response_mode added to OAuthAuthorizationParams return * Added class OauthPostFulfillmentStage Check response_mode in initialization * Corrected typo * Removed separate class Added handling for FORM_POST in create_response_uri Added handling for FORM_POST in return class * Fixed pylint error (trailing-whitespace) Removed comment * Reformatted authorize.py with black --- authentik/providers/oauth2/models.py | 1 + authentik/providers/oauth2/views/authorize.py | 60 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py index 28a7ac91c..f85887662 100644 --- a/authentik/providers/oauth2/models.py +++ b/authentik/providers/oauth2/models.py @@ -50,6 +50,7 @@ class ResponseMode(models.TextChoices): QUERY = "query" FRAGMENT = "fragment" + FORM_POST = "form_post" class SubModes(models.TextChoices): diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py index 5b08fddd1..eeda58dd5 100644 --- a/authentik/providers/oauth2/views/authorize.py +++ b/authentik/providers/oauth2/views/authorize.py @@ -15,6 +15,7 @@ from structlog.stdlib import get_logger from authentik.core.models import Application from authentik.events.models import Event, EventAction from authentik.events.utils import get_user +from authentik.flows.challenge import ChallengeTypes, HttpChallengeResponse from authentik.flows.models import in_memory_stage from authentik.flows.planner import ( PLAN_CONTEXT_APPLICATION, @@ -50,6 +51,7 @@ from authentik.providers.oauth2.models import ( ) from authentik.providers.oauth2.utils import HttpResponseRedirectScheme from authentik.providers.oauth2.views.userinfo import UserInfoView +from authentik.providers.saml.views.flows import AutosubmitChallenge from authentik.stages.consent.models import ConsentMode, ConsentStage from authentik.stages.consent.stage import ( PLAN_CONTEXT_CONSENT_HEADER, @@ -74,6 +76,7 @@ class OAuthAuthorizationParams: client_id: str redirect_uri: str response_type: str + response_mode: Optional[str] scope: list[str] state: str nonce: Optional[str] @@ -125,11 +128,22 @@ class OAuthAuthorizationParams: LOGGER.warning("Invalid response type", type=response_type) raise AuthorizeError(redirect_uri, "unsupported_response_type", "", state) + # Validate and check the response_mode against the predefined dict + # Set to Query or Fragment if not defined in request + response_mode = query_dict.get("response_mode", False) + + if response_mode not in ResponseMode.values: + response_mode = ResponseMode.QUERY + + if grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: + response_mode = ResponseMode.FRAGMENT + max_age = query_dict.get("max_age") return OAuthAuthorizationParams( client_id=query_dict.get("client_id", ""), redirect_uri=redirect_uri, response_type=response_type, + response_mode=response_mode, grant_type=grant_type, scope=query_dict.get("scope", "").split(), state=state, @@ -248,6 +262,26 @@ class OAuthFulfillmentStage(StageView): def redirect(self, uri: str) -> HttpResponse: """Redirect using HttpResponseRedirectScheme, compatible with non-http schemes""" parsed = urlparse(uri) + + if self.params.response_mode == ResponseMode.FORM_POST: + query_params = parse_qs(parsed.query) + + challenge = AutosubmitChallenge( + data={ + "type": ChallengeTypes.NATIVE.value, + "component": "ak-stage-autosubmit", + "title": "Redirecting back to application...", + "url": self.params.redirect_uri, + "attrs": query_params, + } + ) + + challenge.is_valid() + + return HttpChallengeResponse( + challenge=challenge, + ) + return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme]) def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @@ -304,29 +338,29 @@ class OAuthFulfillmentStage(StageView): code = self.params.create_code(self.request) code.save(force_insert=True) - query_dict = self.request.POST if self.request.method == "POST" else self.request.GET - response_mode = ResponseMode.QUERY - # Get response mode from url param, otherwise decide based on grant type - if "response_mode" in query_dict: - response_mode = query_dict["response_mode"] - elif self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: - response_mode = ResponseMode.QUERY - elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: - response_mode = ResponseMode.FRAGMENT - - if response_mode == ResponseMode.QUERY: + if self.params.response_mode == ResponseMode.QUERY: query_params["code"] = code.code query_params["state"] = [str(self.params.state) if self.params.state else ""] uri = uri._replace(query=urlencode(query_params, doseq=True)) return urlunsplit(uri) - if response_mode == ResponseMode.FRAGMENT: + + if self.params.response_mode == ResponseMode.FRAGMENT: query_fragment = self.create_implicit_response(code) uri = uri._replace( fragment=uri.fragment + urlencode(query_fragment, doseq=True), ) + return urlunsplit(uri) + + if self.params.response_mode == ResponseMode.FORM_POST: + post_params = self.create_implicit_response(code) + + uri = uri._replace(query=urlencode(post_params, doseq=True)) + + return urlunsplit(uri) + raise OAuth2Error() except OAuth2Error as error: LOGGER.warning("Error when trying to create response uri", error=error) @@ -502,7 +536,9 @@ class AuthorizationFlowInitView(PolicyAccessView): 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",