Repository: goauthentik/authentik

Issue:
FR: Support for response_mode: form_post
**Is your feature request related to a problem? Please describe.**
We have a client application that is meant to use Azure as an IDP, and I've managed to customize most of the authentik flow to be able to use Authentik as a provider. The last part of this is that the OpenID Connect/Oauth2-provider currently doesn't support response_mode: form_post, which would be needed for this compatibility.

**Describe the solution you'd like**
Modify the OpenID Connect/Oauth2 provider for use with the response mode form_post

**Additional context**
I think compatibility with applications that are made for Azure authentication would enhance the usability aspect of Authentik a great deal. I'll look into getting a PR together later this week/beginning of next week when I have some spare time, just thought I'd post here as well in case anyone else is interested/working on it/has a solution already.



Discussion:
After looking at it for a bit, would maybe @BeryJu be able to provide a (very brief) representation of how the OIDC-flow, mostly the latter stages moves through the code? 

My thinking at the moment is I'd have to amend the ResponseMode in providers/oauth2/models.py with form_post, modifying OauthFulfillmentStage providers/oauth2/views/authorize.py to check for the response_mode and create a separate view for the "post"-redirect with an auto-submitting form for posting the data back to the RP.
I think I had a stash somewhere where I basically did that, but you're essentially correct, continue to check the `response_mode` like currently to make sure only valid options allowed.

For the auto-submitting form you can look at the SAML provider which already does this, see https://github.com/goauthentik/authentik/blob/master/authentik/providers/saml/views/flows.py#L86
Fantastic, thanks! I'll try to pull together a PR for this end of this or next week.
@BeryJu Correct me if I'm wrong, only been looking at this for a short while compared to you, but on the following lines: 

https://github.com/goauthentik/authentik/blob/da28bb7d3ce9fa558815bb927029f2e9e36e769f/authentik/providers/oauth2/views/authorize.py#L307-L315

The _if "response_mode" in query_dict_ is never going to be true, since the parameters passed to this view are in string form in the "query" parameter of the request, wouldn't it be either QueryDict(self.request.X['query']), parse_qs(query_dict['query']) or something like the following?

```python
if "response_mode" in query_dict['query']:
    response_mode = parse_qs(query_dict['query'])['response_mode']
```

Haven't tested this with multiple request/response types, only with the code id_token-flow 

Resolving patch:
diff --git a/authentik/providers/oauth2/models.py b/authentik/providers/oauth2/models.py
--- 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
--- a/authentik/providers/oauth2/views/authorize.py
+++ b/authentik/providers/oauth2/views/authorize.py
@@ -15,6 +15,7 @@
 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.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 @@ def from_request(request: HttpRequest) -> "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 @@ def create_response_uri(self) -> str:
                 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 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
                     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",


Test patch: