authentik.providers.oauth2.tests.test_end_session

Test OAuth2 End Session (RP-Initiated Logout) implementation

  1"""Test OAuth2 End Session (RP-Initiated Logout) implementation"""
  2
  3from django.test import RequestFactory
  4from django.urls import reverse
  5
  6from authentik.core.models import Application
  7from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
  8from authentik.lib.generators import generate_id
  9from authentik.providers.oauth2.models import (
 10    OAuth2Provider,
 11    RedirectURI,
 12    RedirectURIMatchingMode,
 13    RedirectURIType,
 14)
 15from authentik.providers.oauth2.tests.utils import OAuthTestCase
 16from authentik.providers.oauth2.views.end_session import EndSessionView
 17
 18
 19class TestEndSessionView(OAuthTestCase):
 20    """Test EndSessionView validation"""
 21
 22    def setUp(self) -> None:
 23        super().setUp()
 24        self.user = create_test_admin_user()
 25        self.invalidation_flow = create_test_flow()
 26        self.app = Application.objects.create(name=generate_id(), slug="test-app")
 27        self.provider = OAuth2Provider.objects.create(
 28            name=generate_id(),
 29            authorization_flow=create_test_flow(),
 30            invalidation_flow=self.invalidation_flow,
 31            redirect_uris=[
 32                RedirectURI(
 33                    RedirectURIMatchingMode.STRICT,
 34                    "http://testserver/callback",
 35                    RedirectURIType.AUTHORIZATION,
 36                ),
 37                RedirectURI(
 38                    RedirectURIMatchingMode.STRICT,
 39                    "http://testserver/logout",
 40                    RedirectURIType.LOGOUT,
 41                ),
 42                RedirectURI(
 43                    RedirectURIMatchingMode.REGEX,
 44                    r"https://.*\.example\.com/logout",
 45                    RedirectURIType.LOGOUT,
 46                ),
 47            ],
 48        )
 49        self.app.provider = self.provider
 50        self.app.save()
 51        # Ensure brand has an invalidation flow
 52        self.brand = create_test_brand()
 53        self.brand.flow_invalidation = self.invalidation_flow
 54        self.brand.save()
 55
 56    def _id_token_hint(self, host: str) -> str:
 57        """Issue a valid id_token_hint for the test provider under the given host."""
 58        return self.provider.encode(
 59            {
 60                "iss": f"http://{host}/application/o/{self.app.slug}/",
 61                "aud": self.provider.client_id,
 62                "sub": str(self.user.pk),
 63            }
 64        )
 65
 66    def test_post_logout_redirect_uri_strict_match(self):
 67        """Test strict URI matching redirects to flow"""
 68        self.client.force_login(self.user)
 69        response = self.client.get(
 70            reverse(
 71                "authentik_providers_oauth2:end-session",
 72                kwargs={"application_slug": self.app.slug},
 73            ),
 74            {
 75                "post_logout_redirect_uri": "http://testserver/logout",
 76                "id_token_hint": self._id_token_hint(self.brand.domain),
 77            },
 78            HTTP_HOST=self.brand.domain,
 79        )
 80        # Should redirect to the invalidation flow
 81        self.assertEqual(response.status_code, 302)
 82        self.assertIn(self.invalidation_flow.slug, response.url)
 83
 84    def test_post_logout_redirect_uri_strict_no_match(self):
 85        """Test strict URI not matching returns an error and does not start logout flow.
 86
 87        Required by OIDC RP-Initiated Logout 1.0: on an unregistered
 88        post_logout_redirect_uri, the OP MUST NOT redirect and MUST NOT proceed with
 89        logout that targets the RP.
 90        """
 91        self.client.force_login(self.user)
 92        invalid_uri = "http://testserver/other"
 93        response = self.client.get(
 94            reverse(
 95                "authentik_providers_oauth2:end-session",
 96                kwargs={"application_slug": self.app.slug},
 97            ),
 98            {
 99                "post_logout_redirect_uri": invalid_uri,
100                "id_token_hint": self._id_token_hint(self.brand.domain),
101            },
102            HTTP_HOST=self.brand.domain,
103        )
104        self.assertEqual(response.status_code, 400)
105        self.assertNotIn(invalid_uri, response.content.decode())
106
107    def test_post_logout_redirect_uri_regex_match(self):
108        """Test regex URI matching redirects to flow"""
109        self.client.force_login(self.user)
110        response = self.client.get(
111            reverse(
112                "authentik_providers_oauth2:end-session",
113                kwargs={"application_slug": self.app.slug},
114            ),
115            {
116                "post_logout_redirect_uri": "https://app.example.com/logout",
117                "id_token_hint": self._id_token_hint(self.brand.domain),
118            },
119            HTTP_HOST=self.brand.domain,
120        )
121        # Should redirect to the invalidation flow
122        self.assertEqual(response.status_code, 302)
123        self.assertIn(self.invalidation_flow.slug, response.url)
124
125    def test_post_logout_redirect_uri_regex_no_match(self):
126        """Test regex URI not matching returns an error and does not start logout flow."""
127        self.client.force_login(self.user)
128        invalid_uri = "https://malicious.com/logout"
129        response = self.client.get(
130            reverse(
131                "authentik_providers_oauth2:end-session",
132                kwargs={"application_slug": self.app.slug},
133            ),
134            {
135                "post_logout_redirect_uri": invalid_uri,
136                "id_token_hint": self._id_token_hint(self.brand.domain),
137            },
138            HTTP_HOST=self.brand.domain,
139        )
140        self.assertEqual(response.status_code, 400)
141        self.assertNotIn(invalid_uri, response.content.decode())
142
143    def test_state_parameter_appended_to_uri(self):
144        """Test state parameter is appended to validated redirect URI"""
145        factory = RequestFactory()
146        request = factory.get(
147            "/end-session/",
148            {
149                "post_logout_redirect_uri": "http://testserver/logout",
150                "state": "test-state-123",
151                "id_token_hint": self._id_token_hint("testserver"),
152            },
153        )
154        request.user = self.user
155        request.brand = self.brand
156
157        view = EndSessionView()
158        view.request = request
159        view.kwargs = {"application_slug": self.app.slug}
160        view.resolve_provider_application()
161        view.validate()
162
163        self.assertIn("state=test-state-123", view.post_logout_redirect_uri)
164
165    def test_post_method(self):
166        """Test POST requests work same as GET"""
167        self.client.force_login(self.user)
168        response = self.client.post(
169            reverse(
170                "authentik_providers_oauth2:end-session",
171                kwargs={"application_slug": self.app.slug},
172            ),
173            {
174                "post_logout_redirect_uri": "http://testserver/logout",
175                "state": "xyz789",
176                "id_token_hint": self._id_token_hint(self.brand.domain),
177            },
178            HTTP_HOST=self.brand.domain,
179        )
180        self.assertEqual(response.status_code, 302)
181
182
183class TestEndSessionAPI(OAuthTestCase):
184    """Test End Session API functionality"""
185
186    def setUp(self) -> None:
187        super().setUp()
188        self.user = create_test_admin_user()
189        self.client.force_login(self.user)
190
191    def test_post_logout_redirect_uris_create(self):
192        """Test creating provider with post_logout redirect_uris"""
193        response = self.client.post(
194            reverse("authentik_api:oauth2provider-list"),
195            data={
196                "name": generate_id(),
197                "authorization_flow": create_test_flow().pk,
198                "invalidation_flow": create_test_flow().pk,
199                "redirect_uris": [
200                    {
201                        "matching_mode": "strict",
202                        "url": "http://testserver/callback",
203                        "redirect_uri_type": "authorization",
204                    },
205                    {
206                        "matching_mode": "strict",
207                        "url": "http://testserver/logout",
208                        "redirect_uri_type": "logout",
209                    },
210                    {
211                        "matching_mode": "regex",
212                        "url": "https://.*\\.example\\.com/logout",
213                        "redirect_uri_type": "logout",
214                    },
215                ],
216            },
217            content_type="application/json",
218        )
219        self.assertEqual(response.status_code, 201)
220        provider_data = response.json()
221        post_logout_uris = [
222            u for u in provider_data["redirect_uris"] if u["redirect_uri_type"] == "logout"
223        ]
224        self.assertEqual(len(post_logout_uris), 2)
225
226    def test_post_logout_redirect_uris_invalid_regex(self):
227        """Test that invalid regex patterns are rejected"""
228        response = self.client.post(
229            reverse("authentik_api:oauth2provider-list"),
230            data={
231                "name": generate_id(),
232                "authorization_flow": create_test_flow().pk,
233                "invalidation_flow": create_test_flow().pk,
234                "redirect_uris": [
235                    {
236                        "matching_mode": "strict",
237                        "url": "http://testserver/callback",
238                        "redirect_uri_type": "authorization",
239                    },
240                    {
241                        "matching_mode": "regex",
242                        "url": "**invalid**",
243                        "redirect_uri_type": "logout",
244                    },
245                ],
246            },
247            content_type="application/json",
248        )
249        self.assertEqual(response.status_code, 400)
250        self.assertIn("redirect_uris", response.json())
251
252    def test_post_logout_redirect_uris_update(self):
253        """Test updating redirect_uris with logout type"""
254        # First create a provider
255        provider = OAuth2Provider.objects.create(
256            name=generate_id(),
257            authorization_flow=create_test_flow(),
258            redirect_uris=[
259                RedirectURI(
260                    RedirectURIMatchingMode.STRICT,
261                    "http://testserver/callback",
262                    RedirectURIType.AUTHORIZATION,
263                ),
264            ],
265        )
266
267        # Update with post_logout redirect URIs
268        response = self.client.patch(
269            reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider.pk}),
270            data={
271                "redirect_uris": [
272                    {
273                        "matching_mode": "strict",
274                        "url": "http://testserver/callback",
275                        "redirect_uri_type": "authorization",
276                    },
277                    {
278                        "matching_mode": "strict",
279                        "url": "http://testserver/logout",
280                        "redirect_uri_type": "logout",
281                    },
282                ],
283            },
284            content_type="application/json",
285        )
286        self.assertEqual(response.status_code, 200)
287
288        # Verify the update
289        provider.refresh_from_db()
290        self.assertEqual(len(provider.post_logout_redirect_uris), 1)
291        self.assertEqual(provider.post_logout_redirect_uris[0].url, "http://testserver/logout")
class TestEndSessionView(authentik.providers.oauth2.tests.utils.OAuthTestCase):
 20class TestEndSessionView(OAuthTestCase):
 21    """Test EndSessionView validation"""
 22
 23    def setUp(self) -> None:
 24        super().setUp()
 25        self.user = create_test_admin_user()
 26        self.invalidation_flow = create_test_flow()
 27        self.app = Application.objects.create(name=generate_id(), slug="test-app")
 28        self.provider = OAuth2Provider.objects.create(
 29            name=generate_id(),
 30            authorization_flow=create_test_flow(),
 31            invalidation_flow=self.invalidation_flow,
 32            redirect_uris=[
 33                RedirectURI(
 34                    RedirectURIMatchingMode.STRICT,
 35                    "http://testserver/callback",
 36                    RedirectURIType.AUTHORIZATION,
 37                ),
 38                RedirectURI(
 39                    RedirectURIMatchingMode.STRICT,
 40                    "http://testserver/logout",
 41                    RedirectURIType.LOGOUT,
 42                ),
 43                RedirectURI(
 44                    RedirectURIMatchingMode.REGEX,
 45                    r"https://.*\.example\.com/logout",
 46                    RedirectURIType.LOGOUT,
 47                ),
 48            ],
 49        )
 50        self.app.provider = self.provider
 51        self.app.save()
 52        # Ensure brand has an invalidation flow
 53        self.brand = create_test_brand()
 54        self.brand.flow_invalidation = self.invalidation_flow
 55        self.brand.save()
 56
 57    def _id_token_hint(self, host: str) -> str:
 58        """Issue a valid id_token_hint for the test provider under the given host."""
 59        return self.provider.encode(
 60            {
 61                "iss": f"http://{host}/application/o/{self.app.slug}/",
 62                "aud": self.provider.client_id,
 63                "sub": str(self.user.pk),
 64            }
 65        )
 66
 67    def test_post_logout_redirect_uri_strict_match(self):
 68        """Test strict URI matching redirects to flow"""
 69        self.client.force_login(self.user)
 70        response = self.client.get(
 71            reverse(
 72                "authentik_providers_oauth2:end-session",
 73                kwargs={"application_slug": self.app.slug},
 74            ),
 75            {
 76                "post_logout_redirect_uri": "http://testserver/logout",
 77                "id_token_hint": self._id_token_hint(self.brand.domain),
 78            },
 79            HTTP_HOST=self.brand.domain,
 80        )
 81        # Should redirect to the invalidation flow
 82        self.assertEqual(response.status_code, 302)
 83        self.assertIn(self.invalidation_flow.slug, response.url)
 84
 85    def test_post_logout_redirect_uri_strict_no_match(self):
 86        """Test strict URI not matching returns an error and does not start logout flow.
 87
 88        Required by OIDC RP-Initiated Logout 1.0: on an unregistered
 89        post_logout_redirect_uri, the OP MUST NOT redirect and MUST NOT proceed with
 90        logout that targets the RP.
 91        """
 92        self.client.force_login(self.user)
 93        invalid_uri = "http://testserver/other"
 94        response = self.client.get(
 95            reverse(
 96                "authentik_providers_oauth2:end-session",
 97                kwargs={"application_slug": self.app.slug},
 98            ),
 99            {
100                "post_logout_redirect_uri": invalid_uri,
101                "id_token_hint": self._id_token_hint(self.brand.domain),
102            },
103            HTTP_HOST=self.brand.domain,
104        )
105        self.assertEqual(response.status_code, 400)
106        self.assertNotIn(invalid_uri, response.content.decode())
107
108    def test_post_logout_redirect_uri_regex_match(self):
109        """Test regex URI matching redirects to flow"""
110        self.client.force_login(self.user)
111        response = self.client.get(
112            reverse(
113                "authentik_providers_oauth2:end-session",
114                kwargs={"application_slug": self.app.slug},
115            ),
116            {
117                "post_logout_redirect_uri": "https://app.example.com/logout",
118                "id_token_hint": self._id_token_hint(self.brand.domain),
119            },
120            HTTP_HOST=self.brand.domain,
121        )
122        # Should redirect to the invalidation flow
123        self.assertEqual(response.status_code, 302)
124        self.assertIn(self.invalidation_flow.slug, response.url)
125
126    def test_post_logout_redirect_uri_regex_no_match(self):
127        """Test regex URI not matching returns an error and does not start logout flow."""
128        self.client.force_login(self.user)
129        invalid_uri = "https://malicious.com/logout"
130        response = self.client.get(
131            reverse(
132                "authentik_providers_oauth2:end-session",
133                kwargs={"application_slug": self.app.slug},
134            ),
135            {
136                "post_logout_redirect_uri": invalid_uri,
137                "id_token_hint": self._id_token_hint(self.brand.domain),
138            },
139            HTTP_HOST=self.brand.domain,
140        )
141        self.assertEqual(response.status_code, 400)
142        self.assertNotIn(invalid_uri, response.content.decode())
143
144    def test_state_parameter_appended_to_uri(self):
145        """Test state parameter is appended to validated redirect URI"""
146        factory = RequestFactory()
147        request = factory.get(
148            "/end-session/",
149            {
150                "post_logout_redirect_uri": "http://testserver/logout",
151                "state": "test-state-123",
152                "id_token_hint": self._id_token_hint("testserver"),
153            },
154        )
155        request.user = self.user
156        request.brand = self.brand
157
158        view = EndSessionView()
159        view.request = request
160        view.kwargs = {"application_slug": self.app.slug}
161        view.resolve_provider_application()
162        view.validate()
163
164        self.assertIn("state=test-state-123", view.post_logout_redirect_uri)
165
166    def test_post_method(self):
167        """Test POST requests work same as GET"""
168        self.client.force_login(self.user)
169        response = self.client.post(
170            reverse(
171                "authentik_providers_oauth2:end-session",
172                kwargs={"application_slug": self.app.slug},
173            ),
174            {
175                "post_logout_redirect_uri": "http://testserver/logout",
176                "state": "xyz789",
177                "id_token_hint": self._id_token_hint(self.brand.domain),
178            },
179            HTTP_HOST=self.brand.domain,
180        )
181        self.assertEqual(response.status_code, 302)

Test EndSessionView validation

def setUp(self) -> None:
23    def setUp(self) -> None:
24        super().setUp()
25        self.user = create_test_admin_user()
26        self.invalidation_flow = create_test_flow()
27        self.app = Application.objects.create(name=generate_id(), slug="test-app")
28        self.provider = OAuth2Provider.objects.create(
29            name=generate_id(),
30            authorization_flow=create_test_flow(),
31            invalidation_flow=self.invalidation_flow,
32            redirect_uris=[
33                RedirectURI(
34                    RedirectURIMatchingMode.STRICT,
35                    "http://testserver/callback",
36                    RedirectURIType.AUTHORIZATION,
37                ),
38                RedirectURI(
39                    RedirectURIMatchingMode.STRICT,
40                    "http://testserver/logout",
41                    RedirectURIType.LOGOUT,
42                ),
43                RedirectURI(
44                    RedirectURIMatchingMode.REGEX,
45                    r"https://.*\.example\.com/logout",
46                    RedirectURIType.LOGOUT,
47                ),
48            ],
49        )
50        self.app.provider = self.provider
51        self.app.save()
52        # Ensure brand has an invalidation flow
53        self.brand = create_test_brand()
54        self.brand.flow_invalidation = self.invalidation_flow
55        self.brand.save()

Hook method for setting up the test fixture before exercising it.

def test_post_logout_redirect_uri_strict_match(self):
67    def test_post_logout_redirect_uri_strict_match(self):
68        """Test strict URI matching redirects to flow"""
69        self.client.force_login(self.user)
70        response = self.client.get(
71            reverse(
72                "authentik_providers_oauth2:end-session",
73                kwargs={"application_slug": self.app.slug},
74            ),
75            {
76                "post_logout_redirect_uri": "http://testserver/logout",
77                "id_token_hint": self._id_token_hint(self.brand.domain),
78            },
79            HTTP_HOST=self.brand.domain,
80        )
81        # Should redirect to the invalidation flow
82        self.assertEqual(response.status_code, 302)
83        self.assertIn(self.invalidation_flow.slug, response.url)

Test strict URI matching redirects to flow

def test_post_logout_redirect_uri_strict_no_match(self):
 85    def test_post_logout_redirect_uri_strict_no_match(self):
 86        """Test strict URI not matching returns an error and does not start logout flow.
 87
 88        Required by OIDC RP-Initiated Logout 1.0: on an unregistered
 89        post_logout_redirect_uri, the OP MUST NOT redirect and MUST NOT proceed with
 90        logout that targets the RP.
 91        """
 92        self.client.force_login(self.user)
 93        invalid_uri = "http://testserver/other"
 94        response = self.client.get(
 95            reverse(
 96                "authentik_providers_oauth2:end-session",
 97                kwargs={"application_slug": self.app.slug},
 98            ),
 99            {
100                "post_logout_redirect_uri": invalid_uri,
101                "id_token_hint": self._id_token_hint(self.brand.domain),
102            },
103            HTTP_HOST=self.brand.domain,
104        )
105        self.assertEqual(response.status_code, 400)
106        self.assertNotIn(invalid_uri, response.content.decode())

Test strict URI not matching returns an error and does not start logout flow.

Required by OIDC RP-Initiated Logout 1.0: on an unregistered post_logout_redirect_uri, the OP MUST NOT redirect and MUST NOT proceed with logout that targets the RP.

def test_post_logout_redirect_uri_regex_match(self):
108    def test_post_logout_redirect_uri_regex_match(self):
109        """Test regex URI matching redirects to flow"""
110        self.client.force_login(self.user)
111        response = self.client.get(
112            reverse(
113                "authentik_providers_oauth2:end-session",
114                kwargs={"application_slug": self.app.slug},
115            ),
116            {
117                "post_logout_redirect_uri": "https://app.example.com/logout",
118                "id_token_hint": self._id_token_hint(self.brand.domain),
119            },
120            HTTP_HOST=self.brand.domain,
121        )
122        # Should redirect to the invalidation flow
123        self.assertEqual(response.status_code, 302)
124        self.assertIn(self.invalidation_flow.slug, response.url)

Test regex URI matching redirects to flow

def test_post_logout_redirect_uri_regex_no_match(self):
126    def test_post_logout_redirect_uri_regex_no_match(self):
127        """Test regex URI not matching returns an error and does not start logout flow."""
128        self.client.force_login(self.user)
129        invalid_uri = "https://malicious.com/logout"
130        response = self.client.get(
131            reverse(
132                "authentik_providers_oauth2:end-session",
133                kwargs={"application_slug": self.app.slug},
134            ),
135            {
136                "post_logout_redirect_uri": invalid_uri,
137                "id_token_hint": self._id_token_hint(self.brand.domain),
138            },
139            HTTP_HOST=self.brand.domain,
140        )
141        self.assertEqual(response.status_code, 400)
142        self.assertNotIn(invalid_uri, response.content.decode())

Test regex URI not matching returns an error and does not start logout flow.

def test_state_parameter_appended_to_uri(self):
144    def test_state_parameter_appended_to_uri(self):
145        """Test state parameter is appended to validated redirect URI"""
146        factory = RequestFactory()
147        request = factory.get(
148            "/end-session/",
149            {
150                "post_logout_redirect_uri": "http://testserver/logout",
151                "state": "test-state-123",
152                "id_token_hint": self._id_token_hint("testserver"),
153            },
154        )
155        request.user = self.user
156        request.brand = self.brand
157
158        view = EndSessionView()
159        view.request = request
160        view.kwargs = {"application_slug": self.app.slug}
161        view.resolve_provider_application()
162        view.validate()
163
164        self.assertIn("state=test-state-123", view.post_logout_redirect_uri)

Test state parameter is appended to validated redirect URI

def test_post_method(self):
166    def test_post_method(self):
167        """Test POST requests work same as GET"""
168        self.client.force_login(self.user)
169        response = self.client.post(
170            reverse(
171                "authentik_providers_oauth2:end-session",
172                kwargs={"application_slug": self.app.slug},
173            ),
174            {
175                "post_logout_redirect_uri": "http://testserver/logout",
176                "state": "xyz789",
177                "id_token_hint": self._id_token_hint(self.brand.domain),
178            },
179            HTTP_HOST=self.brand.domain,
180        )
181        self.assertEqual(response.status_code, 302)

Test POST requests work same as GET

class TestEndSessionAPI(authentik.providers.oauth2.tests.utils.OAuthTestCase):
184class TestEndSessionAPI(OAuthTestCase):
185    """Test End Session API functionality"""
186
187    def setUp(self) -> None:
188        super().setUp()
189        self.user = create_test_admin_user()
190        self.client.force_login(self.user)
191
192    def test_post_logout_redirect_uris_create(self):
193        """Test creating provider with post_logout redirect_uris"""
194        response = self.client.post(
195            reverse("authentik_api:oauth2provider-list"),
196            data={
197                "name": generate_id(),
198                "authorization_flow": create_test_flow().pk,
199                "invalidation_flow": create_test_flow().pk,
200                "redirect_uris": [
201                    {
202                        "matching_mode": "strict",
203                        "url": "http://testserver/callback",
204                        "redirect_uri_type": "authorization",
205                    },
206                    {
207                        "matching_mode": "strict",
208                        "url": "http://testserver/logout",
209                        "redirect_uri_type": "logout",
210                    },
211                    {
212                        "matching_mode": "regex",
213                        "url": "https://.*\\.example\\.com/logout",
214                        "redirect_uri_type": "logout",
215                    },
216                ],
217            },
218            content_type="application/json",
219        )
220        self.assertEqual(response.status_code, 201)
221        provider_data = response.json()
222        post_logout_uris = [
223            u for u in provider_data["redirect_uris"] if u["redirect_uri_type"] == "logout"
224        ]
225        self.assertEqual(len(post_logout_uris), 2)
226
227    def test_post_logout_redirect_uris_invalid_regex(self):
228        """Test that invalid regex patterns are rejected"""
229        response = self.client.post(
230            reverse("authentik_api:oauth2provider-list"),
231            data={
232                "name": generate_id(),
233                "authorization_flow": create_test_flow().pk,
234                "invalidation_flow": create_test_flow().pk,
235                "redirect_uris": [
236                    {
237                        "matching_mode": "strict",
238                        "url": "http://testserver/callback",
239                        "redirect_uri_type": "authorization",
240                    },
241                    {
242                        "matching_mode": "regex",
243                        "url": "**invalid**",
244                        "redirect_uri_type": "logout",
245                    },
246                ],
247            },
248            content_type="application/json",
249        )
250        self.assertEqual(response.status_code, 400)
251        self.assertIn("redirect_uris", response.json())
252
253    def test_post_logout_redirect_uris_update(self):
254        """Test updating redirect_uris with logout type"""
255        # First create a provider
256        provider = OAuth2Provider.objects.create(
257            name=generate_id(),
258            authorization_flow=create_test_flow(),
259            redirect_uris=[
260                RedirectURI(
261                    RedirectURIMatchingMode.STRICT,
262                    "http://testserver/callback",
263                    RedirectURIType.AUTHORIZATION,
264                ),
265            ],
266        )
267
268        # Update with post_logout redirect URIs
269        response = self.client.patch(
270            reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider.pk}),
271            data={
272                "redirect_uris": [
273                    {
274                        "matching_mode": "strict",
275                        "url": "http://testserver/callback",
276                        "redirect_uri_type": "authorization",
277                    },
278                    {
279                        "matching_mode": "strict",
280                        "url": "http://testserver/logout",
281                        "redirect_uri_type": "logout",
282                    },
283                ],
284            },
285            content_type="application/json",
286        )
287        self.assertEqual(response.status_code, 200)
288
289        # Verify the update
290        provider.refresh_from_db()
291        self.assertEqual(len(provider.post_logout_redirect_uris), 1)
292        self.assertEqual(provider.post_logout_redirect_uris[0].url, "http://testserver/logout")

Test End Session API functionality

def setUp(self) -> None:
187    def setUp(self) -> None:
188        super().setUp()
189        self.user = create_test_admin_user()
190        self.client.force_login(self.user)

Hook method for setting up the test fixture before exercising it.

def test_post_logout_redirect_uris_create(self):
192    def test_post_logout_redirect_uris_create(self):
193        """Test creating provider with post_logout redirect_uris"""
194        response = self.client.post(
195            reverse("authentik_api:oauth2provider-list"),
196            data={
197                "name": generate_id(),
198                "authorization_flow": create_test_flow().pk,
199                "invalidation_flow": create_test_flow().pk,
200                "redirect_uris": [
201                    {
202                        "matching_mode": "strict",
203                        "url": "http://testserver/callback",
204                        "redirect_uri_type": "authorization",
205                    },
206                    {
207                        "matching_mode": "strict",
208                        "url": "http://testserver/logout",
209                        "redirect_uri_type": "logout",
210                    },
211                    {
212                        "matching_mode": "regex",
213                        "url": "https://.*\\.example\\.com/logout",
214                        "redirect_uri_type": "logout",
215                    },
216                ],
217            },
218            content_type="application/json",
219        )
220        self.assertEqual(response.status_code, 201)
221        provider_data = response.json()
222        post_logout_uris = [
223            u for u in provider_data["redirect_uris"] if u["redirect_uri_type"] == "logout"
224        ]
225        self.assertEqual(len(post_logout_uris), 2)

Test creating provider with post_logout redirect_uris

def test_post_logout_redirect_uris_invalid_regex(self):
227    def test_post_logout_redirect_uris_invalid_regex(self):
228        """Test that invalid regex patterns are rejected"""
229        response = self.client.post(
230            reverse("authentik_api:oauth2provider-list"),
231            data={
232                "name": generate_id(),
233                "authorization_flow": create_test_flow().pk,
234                "invalidation_flow": create_test_flow().pk,
235                "redirect_uris": [
236                    {
237                        "matching_mode": "strict",
238                        "url": "http://testserver/callback",
239                        "redirect_uri_type": "authorization",
240                    },
241                    {
242                        "matching_mode": "regex",
243                        "url": "**invalid**",
244                        "redirect_uri_type": "logout",
245                    },
246                ],
247            },
248            content_type="application/json",
249        )
250        self.assertEqual(response.status_code, 400)
251        self.assertIn("redirect_uris", response.json())

Test that invalid regex patterns are rejected

def test_post_logout_redirect_uris_update(self):
253    def test_post_logout_redirect_uris_update(self):
254        """Test updating redirect_uris with logout type"""
255        # First create a provider
256        provider = OAuth2Provider.objects.create(
257            name=generate_id(),
258            authorization_flow=create_test_flow(),
259            redirect_uris=[
260                RedirectURI(
261                    RedirectURIMatchingMode.STRICT,
262                    "http://testserver/callback",
263                    RedirectURIType.AUTHORIZATION,
264                ),
265            ],
266        )
267
268        # Update with post_logout redirect URIs
269        response = self.client.patch(
270            reverse("authentik_api:oauth2provider-detail", kwargs={"pk": provider.pk}),
271            data={
272                "redirect_uris": [
273                    {
274                        "matching_mode": "strict",
275                        "url": "http://testserver/callback",
276                        "redirect_uri_type": "authorization",
277                    },
278                    {
279                        "matching_mode": "strict",
280                        "url": "http://testserver/logout",
281                        "redirect_uri_type": "logout",
282                    },
283                ],
284            },
285            content_type="application/json",
286        )
287        self.assertEqual(response.status_code, 200)
288
289        # Verify the update
290        provider.refresh_from_db()
291        self.assertEqual(len(provider.post_logout_redirect_uris), 1)
292        self.assertEqual(provider.post_logout_redirect_uris[0].url, "http://testserver/logout")

Test updating redirect_uris with logout type