Skip to content

Commit

Permalink
Merge pull request #1500 from maykinmedia/swr/test-oidc-logout-frontc…
Browse files Browse the repository at this point in the history
…hannel

Implement frontchannel OIDC logout flow
  • Loading branch information
alextreme authored Dec 16, 2024
2 parents 94fb79b + 2fda37b commit 89451c6
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 19 deletions.
120 changes: 106 additions & 14 deletions src/open_inwoner/accounts/tests/test_oidc_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from hashlib import md5
from typing import Literal
from unittest.mock import patch
from urllib.parse import urlencode

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase, modify_settings, override_settings
Expand Down Expand Up @@ -643,20 +645,64 @@ def test_logout(self, mock_get_solo):
self.assertFalse(User.objects.filter(email="new_user@example.com").exists())

# enter the logout flow
with requests_mock.Mocker() as m:
m.post("http://localhost:8080/logout")
logout_response = self.client.get(logout_url)
logout_response = self.client.get(logout_url)

self.assertEqual(len(m.request_history), 1)
self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout")
self.assertEqual(m.request_history[0].body, "id_token_hint=foo")
self.assertRedirects(
logout_response,
"http://localhost:8080/logout"
+ "?"
+ urlencode(
dict(
id_token_hint="foo",
post_logout_redirect_uri=f"http://testserver{settings.LOGOUT_REDIRECT_URL}",
)
),
fetch_redirect_response=False,
)

self.assertNotIn("oidc_states", self.client.session)
self.assertNotIn("oidc_id_token", self.client.session)
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)

@patch(
"open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo",
return_value=OpenIDDigiDConfig(
id=1,
enabled=True,
oidc_op_logout_endpoint=None,
),
)
def test_logout_without_sso_logout_configured(self, mock_get_solo):
# set up a user with a non existing email address
user = DigidUserFactory.create(
bsn="123456782", email="existing_user@example.com"
)
self.client.force_login(user)
session = self.client.session
session["oidc_states"] = {
"mock": {
"nonce": "nonce",
"config_class": "accounts.OpenIDDigiDConfig",
}
}
session["oidc_id_token"] = "foo"
session.save()
logout_url = reverse("digid_oidc:logout")

self.assertFalse(User.objects.filter(email="new_user@example.com").exists())

# enter the logout flow
logout_response = self.client.get(logout_url)

self.assertRedirects(
logout_response, reverse("login"), fetch_redirect_response=False
logout_response,
settings.LOGOUT_REDIRECT_URL,
fetch_redirect_response=False,
)

self.assertNotIn("oidc_states", self.client.session)
self.assertNotIn("oidc_id_token", self.client.session)
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)

def test_error_page_direct_access(self):
error_url = reverse("oidc-error")
Expand Down Expand Up @@ -1176,20 +1222,66 @@ def test_logout(self, mock_get_solo):
self.assertFalse(User.objects.filter(email="new_user@example.com").exists())

# enter the logout flow
with requests_mock.Mocker() as m:
m.post("http://localhost:8080/logout")
logout_response = self.client.get(logout_url)
logout_response = self.client.get(logout_url)

self.assertRedirects(
logout_response,
"http://localhost:8080/logout"
+ "?"
+ urlencode(
dict(
id_token_hint="foo",
post_logout_redirect_uri=f"http://testserver{settings.LOGOUT_REDIRECT_URL}",
)
),
fetch_redirect_response=False,
)

self.assertEqual(len(m.request_history), 1)
self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout")
self.assertEqual(m.request_history[0].body, "id_token_hint=foo")
self.assertNotIn("oidc_states", self.client.session)
self.assertNotIn("oidc_id_token", self.client.session)
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)

@patch(
"open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo",
return_value=OpenIDEHerkenningConfig(
id=1,
enabled=True,
legal_subject_claim=["kvk"],
oidc_op_logout_endpoint=None,
),
)
def test_logout_without_sso_logout_configured(self, mock_get_solo):
# set up a user with a non existing email address
user = eHerkenningUserFactory.create(
kvk="12345678", email="existing_user@example.com"
)
self.client.force_login(user)
session = self.client.session
session["oidc_states"] = {
"mock": {
"nonce": "nonce",
"config_class": "accounts.OpenIDEHerkenningConfig",
}
}
session["oidc_id_token"] = "foo"
session[KVK_BRANCH_SESSION_VARIABLE] = None
session.save()
logout_url = reverse("eherkenning_oidc:logout")

self.assertFalse(User.objects.filter(email="new_user@example.com").exists())

# enter the logout flow
logout_response = self.client.get(logout_url)

self.assertRedirects(
logout_response, reverse("login"), fetch_redirect_response=False
logout_response,
settings.LOGOUT_REDIRECT_URL,
fetch_redirect_response=False,
)

self.assertNotIn("oidc_states", self.client.session)
self.assertNotIn("oidc_id_token", self.client.session)
self.assertFalse(logout_response.wsgi_request.user.is_authenticated)

@modify_settings(
MIDDLEWARE={
Expand Down
26 changes: 21 additions & 5 deletions src/open_inwoner/accounts/views/auth_oidc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from urllib.parse import urlencode

from django.conf import settings
from django.contrib import auth, messages
Expand All @@ -12,7 +13,6 @@

from digid_eherkenning.oidc.models import BaseConfig
from digid_eherkenning.oidc.views import OIDCAuthenticationCallbackView
from mozilla_django_oidc_db.utils import do_op_logout
from mozilla_django_oidc_db.views import _OIDC_ERROR_SESSION_KEY, OIDCInit

from ..models import OpenIDDigiDConfig, OpenIDEHerkenningConfig
Expand Down Expand Up @@ -97,16 +97,32 @@ def get_success_url(self):

def get(self, request):
assert self.config_class is not None
config = self.config_class.get_solo()

if id_token := request.session.get("oidc_id_token"):
config = self.config_class.get_solo()
do_op_logout(config, id_token)

id_token = request.session.get("oidc_id_token")
if "oidc_login_next" in request.session:
del request.session["oidc_login_next"]

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

# Try to initiate a frontchannel redirect
if logout_endpoint := config.oidc_op_logout_endpoint:
params = {
# The value MUST have been previously registered with the
# OP, either using the post_logout_redirect_uri
# registration parameter or via another mechanism.
"post_logout_redirect_uri": self.request.build_absolute_uri(
self.get_success_url()
),
}
if id_token:
params["id_token_hint"] = id_token

logout_url = f"{logout_endpoint}?{urlencode(params)}"
return HttpResponseRedirect(logout_url)

logger.warning("No OIDC logout endpoint defined")
return HttpResponseRedirect(self.get_success_url())


Expand Down

0 comments on commit 89451c6

Please sign in to comment.