providers/oauth: post_logout_redirect_uri support (#20011)

* oauth2/providers: add post logout redirect uri to providers

* properly handle post_logout_redirect_uri and frontchannel message to rp

* add backchannel support

* move logout url logic

* hanlde forbidden_uri_schemes on post_logout_redirect_uri

* merge post_logout with redirect_uri

---------

Signed-off-by: Connor Peshek <connor@connorpeshek.me>
Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
Connor Peshek
2026-04-07 03:46:11 -05:00
committed by GitHub
parent 507fe39112
commit 8c3d5f1269
28 changed files with 905 additions and 70 deletions

View File

@@ -21,6 +21,9 @@ PROMPT_CONSENT = "consent"
PROMPT_LOGIN = "login"
PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS = "goauthentik.io/providers/oauth2/iframe_sessions"
PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI = "goauthentik.io/providers/oauth2/post_logout_redirect_uri"
OAUTH2_BINDING = "redirect"
SCOPE_OPENID = "openid"
SCOPE_OPENID_PROFILE = "profile"
@@ -37,6 +40,9 @@ TOKEN_TYPE = "Bearer" # nosec
SCOPE_AUTHENTIK_API = "goauthentik.io/api"
# URI schemes that are forbidden for redirect URIs
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
# Read/write full user (including email)
SCOPE_GITHUB_USER = "user"
# Read user (without email)

View File

@@ -15,6 +15,7 @@ from rest_framework.request import Request
from sentry_sdk import start_span
from structlog.stdlib import BoundLogger, get_logger
from authentik.common.oauth.constants import PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI
from authentik.core.models import Application, User
from authentik.flows.challenge import (
AccessDeniedChallenge,
@@ -300,7 +301,24 @@ class SessionEndStage(ChallengeStageView):
that the user is likely to take after signing out of a provider."""
def get_challenge(self, *args, **kwargs) -> Challenge:
# Check for OIDC post_logout_redirect_uri in context
post_logout_redirect_uri = self.executor.plan.context.get(
PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI
)
if post_logout_redirect_uri:
self.logger.debug(
"SessionEndStage redirecting to post_logout_redirect_uri",
redirect_url=post_logout_redirect_uri,
)
return RedirectChallenge(
data={
"to": post_logout_redirect_uri,
},
)
if not self.request.user.is_authenticated:
# User is logged out with no redirect URI - go to default
return RedirectChallenge(
data={
"to": reverse("authentik_core:root-redirect"),

View File

@@ -27,6 +27,7 @@ from authentik.providers.oauth2.models import (
AccessToken,
OAuth2Provider,
RedirectURIMatchingMode,
RedirectURIType,
ScopeMapping,
)
from authentik.rbac.decorators import permission_required
@@ -37,6 +38,9 @@ class RedirectURISerializer(PassiveSerializer):
matching_mode = ChoiceField(choices=RedirectURIMatchingMode.choices)
url = CharField()
redirect_uri_type = ChoiceField(
choices=RedirectURIType.choices, default=RedirectURIType.AUTHORIZATION, required=False
)
class OAuth2ProviderSerializer(ProviderSerializer):

View File

@@ -97,6 +97,11 @@ class RedirectURIMatchingMode(models.TextChoices):
REGEX = "regex", _("Regular Expression URL matching")
class RedirectURIType(models.TextChoices):
AUTHORIZATION = "authorization", _("Authorization")
LOGOUT = "logout", _("Logout")
class OAuth2LogoutMethod(models.TextChoices):
"""OAuth2/OIDC Logout methods"""
@@ -110,6 +115,7 @@ class RedirectURI:
matching_mode: RedirectURIMatchingMode
url: str
redirect_uri_type: RedirectURIType = RedirectURIType.AUTHORIZATION
class ResponseTypes(models.TextChoices):
@@ -220,7 +226,6 @@ class OAuth2Provider(WebfingerProvider, Provider):
"Frontchannel uses iframes in your browser"
),
)
include_claims_in_id_token = models.BooleanField(
default=True,
verbose_name=_("Include claims in id_token"),
@@ -343,7 +348,12 @@ class OAuth2Provider(WebfingerProvider, Provider):
from_dict(
RedirectURI,
entry,
config=Config(type_hooks={RedirectURIMatchingMode: RedirectURIMatchingMode}),
config=Config(
type_hooks={
RedirectURIMatchingMode: RedirectURIMatchingMode,
RedirectURIType: RedirectURIType,
}
),
)
)
return uris
@@ -355,10 +365,24 @@ class OAuth2Provider(WebfingerProvider, Provider):
cleansed.append(asdict(entry))
self._redirect_uris = cleansed
@property
def authorization_redirect_uris(self) -> list[RedirectURI]:
return [
uri
for uri in self.redirect_uris
if uri.redirect_uri_type == RedirectURIType.AUTHORIZATION
]
@property
def post_logout_redirect_uris(self) -> list[RedirectURI]:
return [
uri for uri in self.redirect_uris if uri.redirect_uri_type == RedirectURIType.LOGOUT
]
@property
def launch_url(self) -> str | None:
"""Guess launch_url based on first redirect_uri"""
redirects = self.redirect_uris
redirects = self.authorization_redirect_uris
if len(redirects) < 1:
return None
main_url = redirects[0].url

View File

@@ -1,13 +1,13 @@
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
from authentik.common.oauth.constants import (
OAUTH2_BINDING,
PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS,
)
from authentik.core.models import AuthenticatedSession, User
from authentik.flows.models import in_memory_stage
from authentik.outposts.tasks import hash_session_key
from authentik.providers.iframe_logout import IframeLogoutStageView
from authentik.providers.oauth2.models import (
AccessToken,
@@ -16,6 +16,7 @@ from authentik.providers.oauth2.models import (
RefreshToken,
)
from authentik.providers.oauth2.tasks import backchannel_logout_notification_dispatch
from authentik.providers.oauth2.utils import build_frontchannel_logout_url
from authentik.stages.user_logout.models import UserLogoutStage
from authentik.stages.user_logout.stage import flow_pre_user_logout
@@ -51,43 +52,22 @@ def handle_flow_pre_user_logout(sender, request, user, executor, **kwargs):
LOGGER.debug("No sessions requiring IFrame frontchannel logout")
return
session_key = auth_session.session.session_key if auth_session.session else None
oidc_sessions = []
for token in oidc_access_tokens:
# Parse the logout URI and add query parameters
parsed_url = urlparse(token.provider.logout_uri)
query_params = {}
query_params["iss"] = token.provider.get_issuer(request)
if auth_session.session:
query_params["sid"] = hash_session_key(auth_session.session.session_key)
# Combine existing query params with new ones
if parsed_url.query:
existing_params = parse_qs(parsed_url.query, keep_blank_values=True)
for key, value in existing_params.items():
if key not in query_params:
query_params[key] = value[0] if len(value) == 1 else value
# Build the final URL with query parameters
logout_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
urlencode(query_params),
parsed_url.fragment,
logout_url = build_frontchannel_logout_url(token.provider, request, session_key)
if logout_url:
oidc_sessions.append(
{
"url": logout_url,
"provider_name": token.provider.name,
"binding": OAUTH2_BINDING,
"provider_type": (
f"{token.provider._meta.app_label}.{token.provider._meta.model_name}"
),
}
)
)
logout_data = {
"url": logout_url,
"provider_name": token.provider.name,
"binding": "redirect",
"provider_type": "oidc",
}
oidc_sessions.append(logout_data)
if oidc_sessions:
executor.plan.context[PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS] = oidc_sessions

View File

@@ -0,0 +1,263 @@
"""Test OAuth2 End Session (RP-Initiated Logout) implementation"""
from django.test import RequestFactory
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import (
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
RedirectURIType,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.end_session import EndSessionView
class TestEndSessionView(OAuthTestCase):
"""Test EndSessionView validation"""
def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.invalidation_flow = create_test_flow()
self.app = Application.objects.create(name=generate_id(), slug="test-app")
self.provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
invalidation_flow=self.invalidation_flow,
redirect_uris=[
RedirectURI(
RedirectURIMatchingMode.STRICT,
"http://testserver/callback",
RedirectURIType.AUTHORIZATION,
),
RedirectURI(
RedirectURIMatchingMode.STRICT,
"http://testserver/logout",
RedirectURIType.LOGOUT,
),
RedirectURI(
RedirectURIMatchingMode.REGEX,
r"https://.*\.example\.com/logout",
RedirectURIType.LOGOUT,
),
],
)
self.app.provider = self.provider
self.app.save()
# Ensure brand has an invalidation flow
self.brand = create_test_brand()
self.brand.flow_invalidation = self.invalidation_flow
self.brand.save()
def test_post_logout_redirect_uri_strict_match(self):
"""Test strict URI matching redirects to flow"""
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_providers_oauth2:end-session",
kwargs={"application_slug": self.app.slug},
),
{"post_logout_redirect_uri": "http://testserver/logout"},
HTTP_HOST=self.brand.domain,
)
# Should redirect to the invalidation flow
self.assertEqual(response.status_code, 302)
self.assertIn(self.invalidation_flow.slug, response.url)
def test_post_logout_redirect_uri_strict_no_match(self):
"""Test strict URI not matching still proceeds with flow (no redirect URI in context)"""
self.client.force_login(self.user)
invalid_uri = "http://testserver/other"
response = self.client.get(
reverse(
"authentik_providers_oauth2:end-session",
kwargs={"application_slug": self.app.slug},
),
{"post_logout_redirect_uri": invalid_uri},
HTTP_HOST=self.brand.domain,
)
# Should still redirect to flow, but invalid URI should not be in response
self.assertEqual(response.status_code, 302)
self.assertNotIn(invalid_uri, response.url)
def test_post_logout_redirect_uri_regex_match(self):
"""Test regex URI matching redirects to flow"""
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_providers_oauth2:end-session",
kwargs={"application_slug": self.app.slug},
),
{"post_logout_redirect_uri": "https://app.example.com/logout"},
HTTP_HOST=self.brand.domain,
)
# Should redirect to the invalidation flow
self.assertEqual(response.status_code, 302)
self.assertIn(self.invalidation_flow.slug, response.url)
def test_post_logout_redirect_uri_regex_no_match(self):
"""Test regex URI not matching"""
self.client.force_login(self.user)
invalid_uri = "https://malicious.com/logout"
response = self.client.get(
reverse(
"authentik_providers_oauth2:end-session",
kwargs={"application_slug": self.app.slug},
),
{"post_logout_redirect_uri": invalid_uri},
HTTP_HOST=self.brand.domain,
)
# Should still proceed to flow, but invalid URI should not be in response
self.assertEqual(response.status_code, 302)
self.assertNotIn(invalid_uri, response.url)
def test_state_parameter_appended_to_uri(self):
"""Test state parameter is appended to validated redirect URI"""
factory = RequestFactory()
request = factory.get(
"/end-session/",
{
"post_logout_redirect_uri": "http://testserver/logout",
"state": "test-state-123",
},
)
request.user = self.user
request.brand = self.brand
view = EndSessionView()
view.request = request
view.kwargs = {"application_slug": self.app.slug}
view.resolve_provider_application()
self.assertIn("state=test-state-123", view.post_logout_redirect_uri)
def test_post_method(self):
"""Test POST requests work same as GET"""
self.client.force_login(self.user)
response = self.client.post(
reverse(
"authentik_providers_oauth2:end-session",
kwargs={"application_slug": self.app.slug},
),
{
"post_logout_redirect_uri": "http://testserver/logout",
"state": "xyz789",
},
HTTP_HOST=self.brand.domain,
)
self.assertEqual(response.status_code, 302)
class TestEndSessionAPI(OAuthTestCase):
"""Test End Session API functionality"""
def setUp(self) -> None:
super().setUp()
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_post_logout_redirect_uris_create(self):
"""Test creating provider with post_logout redirect_uris"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{
"matching_mode": "strict",
"url": "http://testserver/callback",
"redirect_uri_type": "authorization",
},
{
"matching_mode": "strict",
"url": "http://testserver/logout",
"redirect_uri_type": "logout",
},
{
"matching_mode": "regex",
"url": "https://.*\\.example\\.com/logout",
"redirect_uri_type": "logout",
},
],
},
content_type="application/json",
)
self.assertEqual(response.status_code, 201)
provider_data = response.json()
post_logout_uris = [
u for u in provider_data["redirect_uris"] if u["redirect_uri_type"] == "logout"
]
self.assertEqual(len(post_logout_uris), 2)
def test_post_logout_redirect_uris_invalid_regex(self):
"""Test that invalid regex patterns are rejected"""
response = self.client.post(
reverse("authentik_api:oauth2provider-list"),
data={
"name": generate_id(),
"authorization_flow": create_test_flow().pk,
"invalidation_flow": create_test_flow().pk,
"redirect_uris": [
{
"matching_mode": "strict",
"url": "http://testserver/callback",
"redirect_uri_type": "authorization",
},
{
"matching_mode": "regex",
"url": "**invalid**",
"redirect_uri_type": "logout",
},
],
},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertIn("redirect_uris", response.json())
def test_post_logout_redirect_uris_update(self):
"""Test updating redirect_uris with logout type"""
# First create a provider
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
redirect_uris=[
RedirectURI(
RedirectURIMatchingMode.STRICT,
"http://testserver/callback",
RedirectURIType.AUTHORIZATION,
),
],
)
# Update with post_logout redirect URIs
response = self.client.patch(
reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider.pk}),
data={
"redirect_uris": [
{
"matching_mode": "strict",
"url": "http://testserver/callback",
"redirect_uri_type": "authorization",
},
{
"matching_mode": "strict",
"url": "http://testserver/logout",
"redirect_uri_type": "logout",
},
],
},
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
# Verify the update
provider.refresh_from_db()
self.assertEqual(len(provider.post_logout_redirect_uris), 1)
self.assertEqual(provider.post_logout_redirect_uris[0].url, "http://testserver/logout")

View File

@@ -7,7 +7,7 @@ from binascii import Error
from hashlib import sha256
from hmac import compare_digest
from typing import Any
from urllib.parse import unquote, urlparse
from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.http.response import HttpResponseRedirect
@@ -267,3 +267,32 @@ def create_logout_token(
payload["sid"] = hash_session_key(session_key)
# Encode the token
return provider.encode(payload, jwt_type="logout+jwt")
def build_frontchannel_logout_url(
provider: OAuth2Provider,
request: HttpRequest,
session_key: str | None = None,
) -> str | None:
"""Build frontchannel logout URL with iss and sid parameters.
Returns None if provider doesn't have a logout_uri configured.
"""
if not provider.logout_uri:
return None
parsed_url = urlparse(provider.logout_uri)
query_params = {"iss": provider.get_issuer(request)}
if session_key:
query_params["sid"] = hash_session_key(session_key)
# Preserve existing query params
if parsed_url.query:
existing_params = parse_qs(parsed_url.query, keep_blank_values=True)
for key, value in existing_params.items():
if key not in query_params:
query_params[key] = value[0] if len(value) == 1 else value
parsed_url = parsed_url._replace(query=urlencode(query_params))
return urlunparse(parsed_url)

View File

@@ -18,6 +18,7 @@ from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.common.oauth.constants import (
FORBIDDEN_URI_SCHEMES,
PKCE_METHOD_PLAIN,
PKCE_METHOD_S256,
PROMPT_CONSENT,
@@ -60,6 +61,7 @@ from authentik.providers.oauth2.models import (
OAuth2Provider,
RedirectURI,
RedirectURIMatchingMode,
RedirectURIType,
ResponseMode,
ResponseTypes,
ScopeMapping,
@@ -78,7 +80,6 @@ PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
FORBIDDEN_URI_SCHEMES = {"javascript", "data", "vbscript"}
@dataclass(slots=True)
@@ -191,7 +192,7 @@ class OAuthAuthorizationParams:
def check_redirect_uri(self):
"""Redirect URI validation."""
allowed_redirect_urls = self.provider.redirect_uris
allowed_redirect_urls = self.provider.authorization_redirect_uris
if not self.redirect_uri:
LOGGER.warning("Missing redirect uri.")
raise RedirectUriError("", allowed_redirect_urls).with_cause("redirect_uri_missing")
@@ -199,10 +200,14 @@ class OAuthAuthorizationParams:
if len(allowed_redirect_urls) < 1:
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
self.provider.redirect_uris = [
RedirectURI(RedirectURIMatchingMode.STRICT, self.redirect_uri)
RedirectURI(
RedirectURIMatchingMode.STRICT,
self.redirect_uri,
RedirectURIType.AUTHORIZATION,
)
]
self.provider.save()
allowed_redirect_urls = self.provider.redirect_uris
allowed_redirect_urls = self.provider.authorization_redirect_uris
match_found = False
for allowed in allowed_redirect_urls:

View File

@@ -1,20 +1,42 @@
"""oauth2 provider end_session Views"""
from re import fullmatch
from urllib.parse import quote, urlparse
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from authentik.core.models import Application
from authentik.common.oauth.constants import (
FORBIDDEN_URI_SCHEMES,
OAUTH2_BINDING,
PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS,
PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI,
)
from authentik.core.models import Application, AuthenticatedSession
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.planner import (
PLAN_CONTEXT_APPLICATION,
FlowPlanner,
)
from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.policies.views import PolicyAccessView
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.iframe_logout import IframeLogoutStageView
from authentik.providers.oauth2.models import (
AccessToken,
OAuth2LogoutMethod,
RedirectURIMatchingMode,
)
from authentik.providers.oauth2.tasks import send_backchannel_logout_request
from authentik.providers.oauth2.utils import build_frontchannel_logout_url
class EndSessionView(PolicyAccessView):
"""Redirect to application's provider's invalidation flow"""
"""OIDC RP-Initiated Logout endpoint"""
flow: Flow
post_logout_redirect_uri: str | None
def resolve_provider_application(self):
self.application = get_object_or_404(Application, slug=self.kwargs["application_slug"])
@@ -25,6 +47,38 @@ class EndSessionView(PolicyAccessView):
if not self.flow:
raise Http404
# Parse end session parameters
query_dict = self.request.POST if self.request.method == "POST" else self.request.GET
state = query_dict.get("state")
request_redirect_uri = query_dict.get("post_logout_redirect_uri")
self.post_logout_redirect_uri = None
# Validate post_logout_redirect_uri against registered URIs
if request_redirect_uri:
if urlparse(request_redirect_uri).scheme in FORBIDDEN_URI_SCHEMES:
raise RequestValidationError(
bad_request_message(
self.request,
"Forbidden URI scheme in post_logout_redirect_uri",
)
)
for allowed in self.provider.post_logout_redirect_uris:
if allowed.matching_mode == RedirectURIMatchingMode.STRICT:
if request_redirect_uri == allowed.url:
self.post_logout_redirect_uri = request_redirect_uri
break
elif allowed.matching_mode == RedirectURIMatchingMode.REGEX:
if fullmatch(allowed.url, request_redirect_uri):
self.post_logout_redirect_uri = request_redirect_uri
break
# Append state to the redirect URI if both are present
if self.post_logout_redirect_uri and state:
separator = "&" if "?" in self.post_logout_redirect_uri else "?"
self.post_logout_redirect_uri = (
f"{self.post_logout_redirect_uri}{separator}state={quote(state, safe='')}"
)
# If IFrame provider logout happens when a saml provider has redirect
# logout enabled, the flow won't make it back without this dispatch
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
@@ -44,11 +98,80 @@ class EndSessionView(PolicyAccessView):
"""Dispatch the flow planner for the invalidation flow"""
planner = FlowPlanner(self.flow)
planner.allow_empty_flows = True
plan = planner.plan(
request,
{
PLAN_CONTEXT_APPLICATION: self.application,
},
# Build flow context with logout parameters
context = {
PLAN_CONTEXT_APPLICATION: self.application,
}
# Get session info for logout notifications and token invalidation
auth_session = AuthenticatedSession.from_request(request, request.user)
# Add validated redirect URI (with state appended) to context if available
if self.post_logout_redirect_uri:
context[PLAN_CONTEXT_POST_LOGOUT_REDIRECT_URI] = self.post_logout_redirect_uri
# Invalidate tokens for this provider/session (RP-initiated logout:
# user stays logged into authentik, only this provider's tokens are revoked)
if request.user.is_authenticated and auth_session:
AccessToken.objects.filter(
user=request.user,
provider=self.provider,
session=auth_session,
).delete()
session_key = (
auth_session.session.session_key if auth_session and auth_session.session else None
)
# Handle frontchannel logout
frontchannel_logout_url = None
if self.provider.logout_method == OAuth2LogoutMethod.FRONTCHANNEL:
frontchannel_logout_url = build_frontchannel_logout_url(
self.provider, request, session_key
)
# Handle backchannel logout
if (
self.provider.logout_method == OAuth2LogoutMethod.BACKCHANNEL
and self.provider.logout_uri
):
# Find access token to get iss and sub for the logout token
access_token = AccessToken.objects.filter(
user=request.user,
provider=self.provider,
session=auth_session,
).first()
if access_token and access_token.id_token:
send_backchannel_logout_request.send(
self.provider.pk,
access_token.id_token.iss,
access_token.id_token.sub,
session_key,
)
# Delete the token to prevent duplicate backchannel logout
# when UserLogoutStage triggers the session deletion signal
access_token.delete()
if frontchannel_logout_url:
context[PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS] = [
{
"url": frontchannel_logout_url,
"provider_name": self.provider.name,
"binding": OAUTH2_BINDING,
"provider_type": (
f"{self.provider._meta.app_label}.{self.provider._meta.model_name}"
),
}
]
plan = planner.plan(request, context)
# Inject iframe logout stage if frontchannel logout is configured
if frontchannel_logout_url:
plan.insert_stage(in_memory_stage(IframeLogoutStageView))
plan.append_stage(in_memory_stage(SessionEndStage))
return plan.to_redirect(self.request, self.flow)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Handle POST requests for logout (same as GET per OIDC spec)"""
return self.get(request, *args, **kwargs)

View File

@@ -74,6 +74,8 @@ class ProviderInfoView(View):
),
"backchannel_logout_supported": True,
"backchannel_logout_session_supported": True,
"frontchannel_logout_supported": True,
"frontchannel_logout_session_supported": True,
"response_types_supported": [
ResponseTypes.CODE,
ResponseTypes.ID_TOKEN,

View File

@@ -241,7 +241,7 @@ class TokenParams:
raise TokenError("invalid_grant")
def __check_redirect_uri(self, request: HttpRequest):
allowed_redirect_urls = self.provider.redirect_uris
allowed_redirect_urls = self.provider.authorization_redirect_uris
# At this point, no provider should have a blank redirect_uri, in case they do
# this will check an empty array and raise an error

View File

@@ -5,6 +5,10 @@ from unittest.mock import Mock
from django.test import RequestFactory, TestCase
from authentik.common.oauth.constants import (
OAUTH2_BINDING,
PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS,
)
from authentik.common.saml.constants import (
RSA_SHA256,
SAML_NAME_ID_FORMAT_EMAIL,
@@ -17,6 +21,7 @@ from authentik.providers.iframe_logout import (
IframeLogoutChallenge,
IframeLogoutStageView,
)
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.saml.models import SAMLLogoutMethods, SAMLProvider
from authentik.providers.saml.native_logout import (
NativeLogoutChallenge,
@@ -295,14 +300,14 @@ class TestIframeLogoutStageView(TestCase):
},
]
# OIDC sessions (pre-processed)
from authentik.common.oauth.constants import PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS
plan.context[PLAN_CONTEXT_OIDC_LOGOUT_IFRAME_SESSIONS] = [
{
"url": "https://oidc.example.com/logout?iss=authentik&sid=abc123",
"provider_name": "oidc-provider",
"binding": "redirect",
"provider_type": "oidc",
"binding": OAUTH2_BINDING,
"provider_type": (
f"{OAuth2Provider._meta.app_label}" f".{OAuth2Provider._meta.model_name}"
),
},
]
stage_view = IframeLogoutStageView(

View File

@@ -10019,6 +10019,14 @@
"type": "string",
"minLength": 1,
"title": "Url"
},
"redirect_uri_type": {
"type": "string",
"enum": [
"authorization",
"logout"
],
"title": "Redirect uri type"
}
},
"required": [

View File

@@ -21,8 +21,9 @@ var _ MappedNullable = &RedirectURI{}
// RedirectURI A single allowed redirect URI entry
type RedirectURI struct {
MatchingMode MatchingModeEnum `json:"matching_mode"`
Url string `json:"url"`
MatchingMode MatchingModeEnum `json:"matching_mode"`
Url string `json:"url"`
RedirectUriType *RedirectUriTypeEnum `json:"redirect_uri_type,omitempty"`
AdditionalProperties map[string]interface{}
}
@@ -36,6 +37,8 @@ func NewRedirectURI(matchingMode MatchingModeEnum, url string) *RedirectURI {
this := RedirectURI{}
this.MatchingMode = matchingMode
this.Url = url
var redirectUriType RedirectUriTypeEnum = REDIRECTURITYPEENUM_AUTHORIZATION
this.RedirectUriType = &redirectUriType
return &this
}
@@ -44,6 +47,8 @@ func NewRedirectURI(matchingMode MatchingModeEnum, url string) *RedirectURI {
// but it doesn't guarantee that properties required by API are set
func NewRedirectURIWithDefaults() *RedirectURI {
this := RedirectURI{}
var redirectUriType RedirectUriTypeEnum = REDIRECTURITYPEENUM_AUTHORIZATION
this.RedirectUriType = &redirectUriType
return &this
}
@@ -95,6 +100,38 @@ func (o *RedirectURI) SetUrl(v string) {
o.Url = v
}
// GetRedirectUriType returns the RedirectUriType field value if set, zero value otherwise.
func (o *RedirectURI) GetRedirectUriType() RedirectUriTypeEnum {
if o == nil || IsNil(o.RedirectUriType) {
var ret RedirectUriTypeEnum
return ret
}
return *o.RedirectUriType
}
// GetRedirectUriTypeOk returns a tuple with the RedirectUriType field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *RedirectURI) GetRedirectUriTypeOk() (*RedirectUriTypeEnum, bool) {
if o == nil || IsNil(o.RedirectUriType) {
return nil, false
}
return o.RedirectUriType, true
}
// HasRedirectUriType returns a boolean if a field has been set.
func (o *RedirectURI) HasRedirectUriType() bool {
if o != nil && !IsNil(o.RedirectUriType) {
return true
}
return false
}
// SetRedirectUriType gets a reference to the given RedirectUriTypeEnum and assigns it to the RedirectUriType field.
func (o *RedirectURI) SetRedirectUriType(v RedirectUriTypeEnum) {
o.RedirectUriType = &v
}
func (o RedirectURI) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -107,6 +144,9 @@ func (o RedirectURI) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["matching_mode"] = o.MatchingMode
toSerialize["url"] = o.Url
if !IsNil(o.RedirectUriType) {
toSerialize["redirect_uri_type"] = o.RedirectUriType
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
@@ -153,6 +193,7 @@ func (o *RedirectURI) UnmarshalJSON(data []byte) (err error) {
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "matching_mode")
delete(additionalProperties, "url")
delete(additionalProperties, "redirect_uri_type")
o.AdditionalProperties = additionalProperties
}

View File

@@ -21,8 +21,9 @@ var _ MappedNullable = &RedirectURIRequest{}
// RedirectURIRequest A single allowed redirect URI entry
type RedirectURIRequest struct {
MatchingMode MatchingModeEnum `json:"matching_mode"`
Url string `json:"url"`
MatchingMode MatchingModeEnum `json:"matching_mode"`
Url string `json:"url"`
RedirectUriType *RedirectUriTypeEnum `json:"redirect_uri_type,omitempty"`
AdditionalProperties map[string]interface{}
}
@@ -36,6 +37,8 @@ func NewRedirectURIRequest(matchingMode MatchingModeEnum, url string) *RedirectU
this := RedirectURIRequest{}
this.MatchingMode = matchingMode
this.Url = url
var redirectUriType RedirectUriTypeEnum = REDIRECTURITYPEENUM_AUTHORIZATION
this.RedirectUriType = &redirectUriType
return &this
}
@@ -44,6 +47,8 @@ func NewRedirectURIRequest(matchingMode MatchingModeEnum, url string) *RedirectU
// but it doesn't guarantee that properties required by API are set
func NewRedirectURIRequestWithDefaults() *RedirectURIRequest {
this := RedirectURIRequest{}
var redirectUriType RedirectUriTypeEnum = REDIRECTURITYPEENUM_AUTHORIZATION
this.RedirectUriType = &redirectUriType
return &this
}
@@ -95,6 +100,38 @@ func (o *RedirectURIRequest) SetUrl(v string) {
o.Url = v
}
// GetRedirectUriType returns the RedirectUriType field value if set, zero value otherwise.
func (o *RedirectURIRequest) GetRedirectUriType() RedirectUriTypeEnum {
if o == nil || IsNil(o.RedirectUriType) {
var ret RedirectUriTypeEnum
return ret
}
return *o.RedirectUriType
}
// GetRedirectUriTypeOk returns a tuple with the RedirectUriType field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *RedirectURIRequest) GetRedirectUriTypeOk() (*RedirectUriTypeEnum, bool) {
if o == nil || IsNil(o.RedirectUriType) {
return nil, false
}
return o.RedirectUriType, true
}
// HasRedirectUriType returns a boolean if a field has been set.
func (o *RedirectURIRequest) HasRedirectUriType() bool {
if o != nil && !IsNil(o.RedirectUriType) {
return true
}
return false
}
// SetRedirectUriType gets a reference to the given RedirectUriTypeEnum and assigns it to the RedirectUriType field.
func (o *RedirectURIRequest) SetRedirectUriType(v RedirectUriTypeEnum) {
o.RedirectUriType = &v
}
func (o RedirectURIRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
@@ -107,6 +144,9 @@ func (o RedirectURIRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["matching_mode"] = o.MatchingMode
toSerialize["url"] = o.Url
if !IsNil(o.RedirectUriType) {
toSerialize["redirect_uri_type"] = o.RedirectUriType
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
@@ -153,6 +193,7 @@ func (o *RedirectURIRequest) UnmarshalJSON(data []byte) (err error) {
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "matching_mode")
delete(additionalProperties, "url")
delete(additionalProperties, "redirect_uri_type")
o.AdditionalProperties = additionalProperties
}

View File

@@ -0,0 +1,111 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// RedirectUriTypeEnum the model 'RedirectUriTypeEnum'
type RedirectUriTypeEnum string
// List of RedirectUriTypeEnum
const (
REDIRECTURITYPEENUM_AUTHORIZATION RedirectUriTypeEnum = "authorization"
REDIRECTURITYPEENUM_LOGOUT RedirectUriTypeEnum = "logout"
)
// All allowed values of RedirectUriTypeEnum enum
var AllowedRedirectUriTypeEnumEnumValues = []RedirectUriTypeEnum{
"authorization",
"logout",
}
func (v *RedirectUriTypeEnum) UnmarshalJSON(src []byte) error {
var value string
err := json.Unmarshal(src, &value)
if err != nil {
return err
}
enumTypeValue := RedirectUriTypeEnum(value)
for _, existing := range AllowedRedirectUriTypeEnumEnumValues {
if existing == enumTypeValue {
*v = enumTypeValue
return nil
}
}
return fmt.Errorf("%+v is not a valid RedirectUriTypeEnum", value)
}
// NewRedirectUriTypeEnumFromValue returns a pointer to a valid RedirectUriTypeEnum
// for the value passed as argument, or an error if the value passed is not allowed by the enum
func NewRedirectUriTypeEnumFromValue(v string) (*RedirectUriTypeEnum, error) {
ev := RedirectUriTypeEnum(v)
if ev.IsValid() {
return &ev, nil
} else {
return nil, fmt.Errorf("invalid value '%v' for RedirectUriTypeEnum: valid values are %v", v, AllowedRedirectUriTypeEnumEnumValues)
}
}
// IsValid return true if the value is valid for the enum, false otherwise
func (v RedirectUriTypeEnum) IsValid() bool {
for _, existing := range AllowedRedirectUriTypeEnumEnumValues {
if existing == v {
return true
}
}
return false
}
// Ptr returns reference to RedirectUriTypeEnum value
func (v RedirectUriTypeEnum) Ptr() *RedirectUriTypeEnum {
return &v
}
type NullableRedirectUriTypeEnum struct {
value *RedirectUriTypeEnum
isSet bool
}
func (v NullableRedirectUriTypeEnum) Get() *RedirectUriTypeEnum {
return v.value
}
func (v *NullableRedirectUriTypeEnum) Set(val *RedirectUriTypeEnum) {
v.value = val
v.isSet = true
}
func (v NullableRedirectUriTypeEnum) IsSet() bool {
return v.isSet
}
func (v *NullableRedirectUriTypeEnum) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableRedirectUriTypeEnum(val *RedirectUriTypeEnum) *NullableRedirectUriTypeEnum {
return &NullableRedirectUriTypeEnum{value: val, isSet: true}
}
func (v NullableRedirectUriTypeEnum) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableRedirectUriTypeEnum) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -1394,6 +1394,8 @@ pub mod redirect_uri;
pub use self::redirect_uri::RedirectUri;
pub mod redirect_uri_request;
pub use self::redirect_uri_request::RedirectUriRequest;
pub mod redirect_uri_type_enum;
pub use self::redirect_uri_type_enum::RedirectUriTypeEnum;
pub mod related_group;
pub use self::related_group::RelatedGroup;
pub mod reputation;

View File

@@ -17,11 +17,17 @@ pub struct RedirectUri {
pub matching_mode: models::MatchingModeEnum,
#[serde(rename = "url")]
pub url: String,
#[serde(rename = "redirect_uri_type", skip_serializing_if = "Option::is_none")]
pub redirect_uri_type: Option<models::RedirectUriTypeEnum>,
}
impl RedirectUri {
/// A single allowed redirect URI entry
pub fn new(matching_mode: models::MatchingModeEnum, url: String) -> RedirectUri {
RedirectUri { matching_mode, url }
RedirectUri {
matching_mode,
url,
redirect_uri_type: None,
}
}
}

View File

@@ -17,11 +17,17 @@ pub struct RedirectUriRequest {
pub matching_mode: models::MatchingModeEnum,
#[serde(rename = "url")]
pub url: String,
#[serde(rename = "redirect_uri_type", skip_serializing_if = "Option::is_none")]
pub redirect_uri_type: Option<models::RedirectUriTypeEnum>,
}
impl RedirectUriRequest {
/// A single allowed redirect URI entry
pub fn new(matching_mode: models::MatchingModeEnum, url: String) -> RedirectUriRequest {
RedirectUriRequest { matching_mode, url }
RedirectUriRequest {
matching_mode,
url,
redirect_uri_type: None,
}
}
}

View File

@@ -0,0 +1,35 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
///
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum RedirectUriTypeEnum {
#[serde(rename = "authorization")]
Authorization,
#[serde(rename = "logout")]
Logout,
}
impl std::fmt::Display for RedirectUriTypeEnum {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Authorization => write!(f, "authorization"),
Self::Logout => write!(f, "logout"),
}
}
}
impl Default for RedirectUriTypeEnum {
fn default() -> RedirectUriTypeEnum {
Self::Authorization
}
}

View File

@@ -14,6 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
/**
* A single allowed redirect URI entry
@@ -33,6 +35,12 @@ export interface RedirectURI {
* @memberof RedirectURI
*/
url: string;
/**
*
* @type {RedirectUriTypeEnum}
* @memberof RedirectURI
*/
redirectUriType?: RedirectUriTypeEnum;
}
/**
@@ -55,6 +63,10 @@ export function RedirectURIFromJSONTyped(json: any, ignoreDiscriminator: boolean
return {
matchingMode: MatchingModeEnumFromJSON(json["matching_mode"]),
url: json["url"],
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -73,5 +85,6 @@ export function RedirectURIToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -14,6 +14,8 @@
import type { MatchingModeEnum } from "./MatchingModeEnum";
import { MatchingModeEnumFromJSON, MatchingModeEnumToJSON } from "./MatchingModeEnum";
import type { RedirectUriTypeEnum } from "./RedirectUriTypeEnum";
import { RedirectUriTypeEnumFromJSON, RedirectUriTypeEnumToJSON } from "./RedirectUriTypeEnum";
/**
* A single allowed redirect URI entry
@@ -33,6 +35,12 @@ export interface RedirectURIRequest {
* @memberof RedirectURIRequest
*/
url: string;
/**
*
* @type {RedirectUriTypeEnum}
* @memberof RedirectURIRequest
*/
redirectUriType?: RedirectUriTypeEnum;
}
/**
@@ -58,6 +66,10 @@ export function RedirectURIRequestFromJSONTyped(
return {
matchingMode: MatchingModeEnumFromJSON(json["matching_mode"]),
url: json["url"],
redirectUriType:
json["redirect_uri_type"] == null
? undefined
: RedirectUriTypeEnumFromJSON(json["redirect_uri_type"]),
};
}
@@ -76,5 +88,6 @@ export function RedirectURIRequestToJSONTyped(
return {
matching_mode: MatchingModeEnumToJSON(value["matchingMode"]),
url: value["url"],
redirect_uri_type: RedirectUriTypeEnumToJSON(value["redirectUriType"]),
};
}

View File

@@ -0,0 +1,57 @@
/* tslint:disable */
/* eslint-disable */
/**
* authentik
* Making authentication simple.
*
* The version of the OpenAPI document: 2026.5.0-rc1
* Contact: hello@goauthentik.io
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
*
* @export
*/
export const RedirectUriTypeEnum = {
Authorization: "authorization",
Logout: "logout",
UnknownDefaultOpenApi: "11184809",
} as const;
export type RedirectUriTypeEnum = (typeof RedirectUriTypeEnum)[keyof typeof RedirectUriTypeEnum];
export function instanceOfRedirectUriTypeEnum(value: any): boolean {
for (const key in RedirectUriTypeEnum) {
if (Object.prototype.hasOwnProperty.call(RedirectUriTypeEnum, key)) {
if (RedirectUriTypeEnum[key as keyof typeof RedirectUriTypeEnum] === value) {
return true;
}
}
}
return false;
}
export function RedirectUriTypeEnumFromJSON(json: any): RedirectUriTypeEnum {
return RedirectUriTypeEnumFromJSONTyped(json, false);
}
export function RedirectUriTypeEnumFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return json as RedirectUriTypeEnum;
}
export function RedirectUriTypeEnumToJSON(value?: RedirectUriTypeEnum | null): any {
return value as any;
}
export function RedirectUriTypeEnumToJSONTyped(
value: any,
ignoreDiscriminator: boolean,
): RedirectUriTypeEnum {
return value as RedirectUriTypeEnum;
}

View File

@@ -698,6 +698,7 @@ export * from "./RedirectStageModeEnum";
export * from "./RedirectStageRequest";
export * from "./RedirectURI";
export * from "./RedirectURIRequest";
export * from "./RedirectUriTypeEnum";
export * from "./RelatedGroup";
export * from "./Reputation";
export * from "./ReputationPolicy";

View File

@@ -52856,6 +52856,10 @@ components:
$ref: '#/components/schemas/MatchingModeEnum'
url:
type: string
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectUriTypeEnum'
default: authorization
required:
- matching_mode
- url
@@ -52868,9 +52872,18 @@ components:
url:
type: string
minLength: 1
redirect_uri_type:
allOf:
- $ref: '#/components/schemas/RedirectUriTypeEnum'
default: authorization
required:
- matching_mode
- url
RedirectUriTypeEnum:
enum:
- authorization
- logout
type: string
RelatedGroup:
type: object
description: Stripped down group serializer to show relevant children/parents

View File

@@ -18,6 +18,7 @@ import {
RACProvider,
RadiusProvider,
RedirectURI,
RedirectUriTypeEnum,
SAMLProvider,
SCIMProvider,
WSFederationProvider,
@@ -85,7 +86,10 @@ function formatRedirectUris(uris: RedirectURI[] = []) {
${uri.url}
(${uri.matchingMode === MatchingModeEnum.Strict
? msg("strict")
: msg("regexp")})
: msg("regexp")},
${uri.redirectUriType === RedirectUriTypeEnum.Logout
? msg("post logout")
: msg("authorization")})
</li>`,
)}
</ul>`

View File

@@ -34,6 +34,7 @@ import {
OAuth2Provider,
OAuth2ProviderLogoutMethodEnum,
RedirectURI,
RedirectUriTypeEnum,
SubModeEnum,
ValidationError,
} from "@goauthentik/api";
@@ -120,10 +121,10 @@ export const issuerModeOptions: RadioOption<IssuerModeEnum>[] = [
const redirectUriHelpMessages: string[] = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
"Valid redirect URIs after a successful authorization or invalidation flow. Also specify any origins here for Implicit flows. Use the type dropdown to designate URIs for authorization or post-logout redirection.",
),
msg(
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
"If no explicit authorization redirect URIs are specified, the first successfully used authorization redirect URI will be saved.",
),
msg(
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
@@ -221,7 +222,11 @@ export function renderForm({
>
<ak-array-input
.items=${provider.redirectUris ?? []}
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
.newItem=${() => ({
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectUriTypeEnum.Authorization,
})}
.row=${(redirectURI: RedirectURI, idx: number) => {
return html`<ak-provider-oauth2-redirect-uri
.redirectURI=${redirectURI}

View File

@@ -4,7 +4,7 @@ import { AKControlElement } from "#elements/ControlElement";
import { LitPropertyRecord } from "#elements/types";
import { ifPresent } from "#elements/utils/attributes";
import { MatchingModeEnum, RedirectURI } from "@goauthentik/api";
import { MatchingModeEnum, RedirectURI, RedirectUriTypeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { css, html } from "lit";
@@ -37,6 +37,7 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
public redirectURI: RedirectURI = {
matchingMode: MatchingModeEnum.Strict,
url: "",
redirectUriType: RedirectUriTypeEnum.Authorization,
};
@property({ type: String, useDefault: true })
@@ -82,6 +83,25 @@ export class OAuth2ProviderRedirectURI extends AKControlElement<RedirectURI> {
${msg("Regex")}
</option>
</select>
<select
name="redirectUriType"
class="pf-c-form-control ak-form-control"
@change=${onChange}
>
<option
value="${RedirectUriTypeEnum.Authorization}"
?selected=${(this.redirectURI.redirectUriType ??
RedirectUriTypeEnum.Authorization) === RedirectUriTypeEnum.Authorization}
>
${msg("Authorization")}
</option>
<option
value="${RedirectUriTypeEnum.Logout}"
?selected=${this.redirectURI.redirectUriType === RedirectUriTypeEnum.Logout}
>
${msg("Post Logout")}
</option>
</select>
<input
type="text"
@change=${onChange}