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
This commit is contained in:
parent
1cb71b5217
commit
d4abf5621e
|
@ -50,6 +50,7 @@ class ResponseMode(models.TextChoices):
|
||||||
|
|
||||||
QUERY = "query"
|
QUERY = "query"
|
||||||
FRAGMENT = "fragment"
|
FRAGMENT = "fragment"
|
||||||
|
FORM_POST = "form_post"
|
||||||
|
|
||||||
|
|
||||||
class SubModes(models.TextChoices):
|
class SubModes(models.TextChoices):
|
||||||
|
|
|
@ -15,6 +15,7 @@ from structlog.stdlib import get_logger
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.utils import get_user
|
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.models import in_memory_stage
|
||||||
from authentik.flows.planner import (
|
from authentik.flows.planner import (
|
||||||
PLAN_CONTEXT_APPLICATION,
|
PLAN_CONTEXT_APPLICATION,
|
||||||
|
@ -50,6 +51,7 @@ from authentik.providers.oauth2.models import (
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
|
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
|
||||||
from authentik.providers.oauth2.views.userinfo import UserInfoView
|
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.models import ConsentMode, ConsentStage
|
||||||
from authentik.stages.consent.stage import (
|
from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_HEADER,
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
|
@ -74,6 +76,7 @@ class OAuthAuthorizationParams:
|
||||||
client_id: str
|
client_id: str
|
||||||
redirect_uri: str
|
redirect_uri: str
|
||||||
response_type: str
|
response_type: str
|
||||||
|
response_mode: Optional[str]
|
||||||
scope: list[str]
|
scope: list[str]
|
||||||
state: str
|
state: str
|
||||||
nonce: Optional[str]
|
nonce: Optional[str]
|
||||||
|
@ -125,11 +128,22 @@ class OAuthAuthorizationParams:
|
||||||
LOGGER.warning("Invalid response type", type=response_type)
|
LOGGER.warning("Invalid response type", type=response_type)
|
||||||
raise AuthorizeError(redirect_uri, "unsupported_response_type", "", state)
|
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")
|
max_age = query_dict.get("max_age")
|
||||||
return OAuthAuthorizationParams(
|
return OAuthAuthorizationParams(
|
||||||
client_id=query_dict.get("client_id", ""),
|
client_id=query_dict.get("client_id", ""),
|
||||||
redirect_uri=redirect_uri,
|
redirect_uri=redirect_uri,
|
||||||
response_type=response_type,
|
response_type=response_type,
|
||||||
|
response_mode=response_mode,
|
||||||
grant_type=grant_type,
|
grant_type=grant_type,
|
||||||
scope=query_dict.get("scope", "").split(),
|
scope=query_dict.get("scope", "").split(),
|
||||||
state=state,
|
state=state,
|
||||||
|
@ -248,6 +262,26 @@ class OAuthFulfillmentStage(StageView):
|
||||||
def redirect(self, uri: str) -> HttpResponse:
|
def redirect(self, uri: str) -> HttpResponse:
|
||||||
"""Redirect using HttpResponseRedirectScheme, compatible with non-http schemes"""
|
"""Redirect using HttpResponseRedirectScheme, compatible with non-http schemes"""
|
||||||
parsed = urlparse(uri)
|
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])
|
return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme])
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
@ -304,29 +338,29 @@ class OAuthFulfillmentStage(StageView):
|
||||||
code = self.params.create_code(self.request)
|
code = self.params.create_code(self.request)
|
||||||
code.save(force_insert=True)
|
code.save(force_insert=True)
|
||||||
|
|
||||||
query_dict = self.request.POST if self.request.method == "POST" else self.request.GET
|
if self.params.response_mode == ResponseMode.QUERY:
|
||||||
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:
|
|
||||||
query_params["code"] = code.code
|
query_params["code"] = code.code
|
||||||
query_params["state"] = [str(self.params.state) if self.params.state else ""]
|
query_params["state"] = [str(self.params.state) if self.params.state else ""]
|
||||||
|
|
||||||
uri = uri._replace(query=urlencode(query_params, doseq=True))
|
uri = uri._replace(query=urlencode(query_params, doseq=True))
|
||||||
return urlunsplit(uri)
|
return urlunsplit(uri)
|
||||||
if response_mode == ResponseMode.FRAGMENT:
|
|
||||||
|
if self.params.response_mode == ResponseMode.FRAGMENT:
|
||||||
query_fragment = self.create_implicit_response(code)
|
query_fragment = self.create_implicit_response(code)
|
||||||
|
|
||||||
uri = uri._replace(
|
uri = uri._replace(
|
||||||
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
|
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
return urlunsplit(uri)
|
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()
|
raise OAuth2Error()
|
||||||
except OAuth2Error as error:
|
except OAuth2Error as error:
|
||||||
LOGGER.warning("Error when trying to create response uri", error=error)
|
LOGGER.warning("Error when trying to create response uri", error=error)
|
||||||
|
@ -502,7 +536,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
||||||
mode=ConsentMode.ALWAYS_REQUIRE,
|
mode=ConsentMode.ALWAYS_REQUIRE,
|
||||||
)
|
)
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
|
|
||||||
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
|
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
|
||||||
|
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
|
|
Reference in New Issue