Skip to content

Commit 89451c6

Browse files
authored
Merge pull request #1500 from maykinmedia/swr/test-oidc-logout-frontchannel
Implement frontchannel OIDC logout flow
2 parents 94fb79b + 2fda37b commit 89451c6

File tree

2 files changed

+127
-19
lines changed

2 files changed

+127
-19
lines changed

src/open_inwoner/accounts/tests/test_oidc_views.py

+106-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from hashlib import md5
22
from typing import Literal
33
from unittest.mock import patch
4+
from urllib.parse import urlencode
45

6+
from django.conf import settings
57
from django.contrib.auth import get_user_model
68
from django.core.exceptions import ValidationError
79
from django.test import TestCase, modify_settings, override_settings
@@ -643,20 +645,64 @@ def test_logout(self, mock_get_solo):
643645
self.assertFalse(User.objects.filter(email="[email protected]").exists())
644646

645647
# enter the logout flow
646-
with requests_mock.Mocker() as m:
647-
m.post("http://localhost:8080/logout")
648-
logout_response = self.client.get(logout_url)
648+
logout_response = self.client.get(logout_url)
649649

650-
self.assertEqual(len(m.request_history), 1)
651-
self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout")
652-
self.assertEqual(m.request_history[0].body, "id_token_hint=foo")
650+
self.assertRedirects(
651+
logout_response,
652+
"http://localhost:8080/logout"
653+
+ "?"
654+
+ urlencode(
655+
dict(
656+
id_token_hint="foo",
657+
post_logout_redirect_uri=f"http://testserver{settings.LOGOUT_REDIRECT_URL}",
658+
)
659+
),
660+
fetch_redirect_response=False,
661+
)
662+
663+
self.assertNotIn("oidc_states", self.client.session)
664+
self.assertNotIn("oidc_id_token", self.client.session)
665+
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)
666+
667+
@patch(
668+
"open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo",
669+
return_value=OpenIDDigiDConfig(
670+
id=1,
671+
enabled=True,
672+
oidc_op_logout_endpoint=None,
673+
),
674+
)
675+
def test_logout_without_sso_logout_configured(self, mock_get_solo):
676+
# set up a user with a non existing email address
677+
user = DigidUserFactory.create(
678+
bsn="123456782", email="[email protected]"
679+
)
680+
self.client.force_login(user)
681+
session = self.client.session
682+
session["oidc_states"] = {
683+
"mock": {
684+
"nonce": "nonce",
685+
"config_class": "accounts.OpenIDDigiDConfig",
686+
}
687+
}
688+
session["oidc_id_token"] = "foo"
689+
session.save()
690+
logout_url = reverse("digid_oidc:logout")
691+
692+
self.assertFalse(User.objects.filter(email="[email protected]").exists())
693+
694+
# enter the logout flow
695+
logout_response = self.client.get(logout_url)
653696

654697
self.assertRedirects(
655-
logout_response, reverse("login"), fetch_redirect_response=False
698+
logout_response,
699+
settings.LOGOUT_REDIRECT_URL,
700+
fetch_redirect_response=False,
656701
)
657702

658703
self.assertNotIn("oidc_states", self.client.session)
659704
self.assertNotIn("oidc_id_token", self.client.session)
705+
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)
660706

661707
def test_error_page_direct_access(self):
662708
error_url = reverse("oidc-error")
@@ -1176,20 +1222,66 @@ def test_logout(self, mock_get_solo):
11761222
self.assertFalse(User.objects.filter(email="[email protected]").exists())
11771223

11781224
# enter the logout flow
1179-
with requests_mock.Mocker() as m:
1180-
m.post("http://localhost:8080/logout")
1181-
logout_response = self.client.get(logout_url)
1225+
logout_response = self.client.get(logout_url)
1226+
1227+
self.assertRedirects(
1228+
logout_response,
1229+
"http://localhost:8080/logout"
1230+
+ "?"
1231+
+ urlencode(
1232+
dict(
1233+
id_token_hint="foo",
1234+
post_logout_redirect_uri=f"http://testserver{settings.LOGOUT_REDIRECT_URL}",
1235+
)
1236+
),
1237+
fetch_redirect_response=False,
1238+
)
11821239

1183-
self.assertEqual(len(m.request_history), 1)
1184-
self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout")
1185-
self.assertEqual(m.request_history[0].body, "id_token_hint=foo")
1240+
self.assertNotIn("oidc_states", self.client.session)
1241+
self.assertNotIn("oidc_id_token", self.client.session)
1242+
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)
1243+
1244+
@patch(
1245+
"open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo",
1246+
return_value=OpenIDEHerkenningConfig(
1247+
id=1,
1248+
enabled=True,
1249+
legal_subject_claim=["kvk"],
1250+
oidc_op_logout_endpoint=None,
1251+
),
1252+
)
1253+
def test_logout_without_sso_logout_configured(self, mock_get_solo):
1254+
# set up a user with a non existing email address
1255+
user = eHerkenningUserFactory.create(
1256+
kvk="12345678", email="[email protected]"
1257+
)
1258+
self.client.force_login(user)
1259+
session = self.client.session
1260+
session["oidc_states"] = {
1261+
"mock": {
1262+
"nonce": "nonce",
1263+
"config_class": "accounts.OpenIDEHerkenningConfig",
1264+
}
1265+
}
1266+
session["oidc_id_token"] = "foo"
1267+
session[KVK_BRANCH_SESSION_VARIABLE] = None
1268+
session.save()
1269+
logout_url = reverse("eherkenning_oidc:logout")
1270+
1271+
self.assertFalse(User.objects.filter(email="[email protected]").exists())
1272+
1273+
# enter the logout flow
1274+
logout_response = self.client.get(logout_url)
11861275

11871276
self.assertRedirects(
1188-
logout_response, reverse("login"), fetch_redirect_response=False
1277+
logout_response,
1278+
settings.LOGOUT_REDIRECT_URL,
1279+
fetch_redirect_response=False,
11891280
)
11901281

11911282
self.assertNotIn("oidc_states", self.client.session)
11921283
self.assertNotIn("oidc_id_token", self.client.session)
1284+
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)
11931285

11941286
@modify_settings(
11951287
MIDDLEWARE={

src/open_inwoner/accounts/views/auth_oidc.py

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from urllib.parse import urlencode
23

34
from django.conf import settings
45
from django.contrib import auth, messages
@@ -12,7 +13,6 @@
1213

1314
from digid_eherkenning.oidc.models import BaseConfig
1415
from digid_eherkenning.oidc.views import OIDCAuthenticationCallbackView
15-
from mozilla_django_oidc_db.utils import do_op_logout
1616
from mozilla_django_oidc_db.views import _OIDC_ERROR_SESSION_KEY, OIDCInit
1717

1818
from ..models import OpenIDDigiDConfig, OpenIDEHerkenningConfig
@@ -97,16 +97,32 @@ def get_success_url(self):
9797

9898
def get(self, request):
9999
assert self.config_class is not None
100+
config = self.config_class.get_solo()
100101

101-
if id_token := request.session.get("oidc_id_token"):
102-
config = self.config_class.get_solo()
103-
do_op_logout(config, id_token)
104-
102+
id_token = request.session.get("oidc_id_token")
105103
if "oidc_login_next" in request.session:
106104
del request.session["oidc_login_next"]
107105

106+
# Always destroy our session first before trying to initiate single-sign out
108107
auth.logout(request)
109108

109+
# Try to initiate a frontchannel redirect
110+
if logout_endpoint := config.oidc_op_logout_endpoint:
111+
params = {
112+
# The value MUST have been previously registered with the
113+
# OP, either using the post_logout_redirect_uri
114+
# registration parameter or via another mechanism.
115+
"post_logout_redirect_uri": self.request.build_absolute_uri(
116+
self.get_success_url()
117+
),
118+
}
119+
if id_token:
120+
params["id_token_hint"] = id_token
121+
122+
logout_url = f"{logout_endpoint}?{urlencode(params)}"
123+
return HttpResponseRedirect(logout_url)
124+
125+
logger.warning("No OIDC logout endpoint defined")
110126
return HttpResponseRedirect(self.get_success_url())
111127

112128

0 commit comments

Comments
 (0)