diff --git a/authentik/providers/oauth2/tests/test_views_authorize.py b/authentik/providers/oauth2/tests/test_views_authorize.py index 92435a484..cc2b5b82d 100644 --- a/authentik/providers/oauth2/tests/test_views_authorize.py +++ b/authentik/providers/oauth2/tests/test_views_authorize.py @@ -166,7 +166,7 @@ class TestViewsAuthorize(TestCase): name="test", client_id="test", authorization_flow=flow, - redirect_uris="http://localhost", + redirect_uris="foo://localhost", ) Application.objects.create(name="app", slug="app", provider=provider) state = generate_client_id() @@ -179,7 +179,7 @@ class TestViewsAuthorize(TestCase): "response_type": "code", "client_id": "test", "state": state, - "redirect_uri": "http://localhost", + "redirect_uri": "foo://localhost", }, ) response = self.client.get( @@ -190,7 +190,7 @@ class TestViewsAuthorize(TestCase): force_str(response.content), { "type": ChallengeTypes.REDIRECT.value, - "to": f"http://localhost?code={code.code}&state={state}", + "to": f"foo://localhost?code={code.code}&state={state}", }, ) diff --git a/authentik/providers/oauth2/utils.py b/authentik/providers/oauth2/utils.py index 18a62411c..a022ef04d 100644 --- a/authentik/providers/oauth2/utils.py +++ b/authentik/providers/oauth2/utils.py @@ -2,10 +2,11 @@ import re from base64 import b64decode from binascii import Error -from typing import Optional +from typing import Any, Optional from urllib.parse import urlparse from django.http import HttpRequest, HttpResponse, JsonResponse +from django.http.response import HttpResponseRedirect from django.utils.cache import patch_vary_headers from structlog.stdlib import get_logger @@ -161,3 +162,18 @@ def protected_resource_view(scopes: list[str]): return view_wrapper return wrapper + + +class HttpResponseRedirectScheme(HttpResponseRedirect): + """HTTP Response to redirect, can be to a non-http scheme""" + + def __init__( + self, + redirect_to: str, + *args: Any, + allowed_schemes: Optional[list[str]] = None, + **kwargs: Any, + ) -> None: + self.allowed_schemes = allowed_schemes or ["http", "https", "ftp"] + # pyright: reportGeneralTypeIssues=false + super().__init__(redirect_to, *args, **kwargs) diff --git a/authentik/providers/oauth2/views/authorize.py b/authentik/providers/oauth2/views/authorize.py index 9c8437a18..12265f992 100644 --- a/authentik/providers/oauth2/views/authorize.py +++ b/authentik/providers/oauth2/views/authorize.py @@ -2,12 +2,12 @@ from dataclasses import dataclass, field from datetime import timedelta from typing import Optional -from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit +from urllib.parse import parse_qs, urlencode, urlparse, urlsplit, urlunsplit from uuid import uuid4 from django.http import HttpRequest, HttpResponse from django.http.response import Http404, HttpResponseBadRequest, HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext as _ from structlog.stdlib import get_logger @@ -46,6 +46,7 @@ from authentik.providers.oauth2.models import ( OAuth2Provider, ResponseTypes, ) +from authentik.providers.oauth2.utils import HttpResponseRedirectScheme from authentik.providers.oauth2.views.userinfo import UserInfoView from authentik.stages.consent.models import ConsentMode, ConsentStage from authentik.stages.consent.stage import ( @@ -233,6 +234,11 @@ class OAuthFulfillmentStage(StageView): params: OAuthAuthorizationParams provider: OAuth2Provider + def redirect(self, uri: str) -> HttpResponse: + """Redirect using HttpResponseRedirectScheme, compatible with non-http schemes""" + parsed = urlparse(uri) + return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme]) + # pylint: disable=unused-argument def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """final Stage of an OAuth2 Flow""" @@ -261,7 +267,7 @@ class OAuthFulfillmentStage(StageView): flow=self.executor.plan.flow_pk, scopes=", ".join(self.params.scope), ).from_http(self.request) - return redirect(self.create_response_uri()) + return self.redirect(self.create_response_uri()) except (ClientIdError, RedirectUriError) as error: error.to_event(application=application).from_http(request) self.executor.stage_invalid() @@ -270,7 +276,7 @@ class OAuthFulfillmentStage(StageView): except AuthorizeError as error: error.to_event(application=application).from_http(request) self.executor.stage_invalid() - return redirect(error.create_uri()) + return self.redirect(error.create_uri()) def create_response_uri(self) -> str: """Create a final Response URI the user is redirected to."""