Skip to content

Commit 728c0a0

Browse files
committed
Move function from models.IDToken to handler
1 parent 0c2b84d commit 728c0a0

File tree

5 files changed

+86
-72
lines changed

5 files changed

+86
-72
lines changed

docs/settings.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ When is set to ``False`` (default) the `OpenID Connect Backchannel Logout <https
409409
extension is not enabled. OpenID Connect Backchannel Logout enables the :term:`Authorization Server` (OpenID Provider) to submit a JWT token to an endpoint controlled by the :term:`Client` (Relying Party)
410410
indicating that a session from the :term:`Resource Owner` (End User) has ended.
411411

412+
OIDC_BACKCHANNEL_LOGOUT_HANDLER
413+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
414+
Default: ``oauth2_provider.handlers.send_backchannel_logout_request``
415+
416+
Upon logout, the :term:`Authorization Server` (OpenID Provider) will look for all ID Tokens associated with the user on applications that support Backchannel Logout. For every id token that is found, the function defined here will be called. The default function can be used as-is, but if you need to override or customize it somehow (e.g, if you do not want to execute these requests on the same HTTP request-response from the user logout view), you can change this setting to any function that takes an IDToken as the first parameter.
417+
412418

413419
OIDC_RESPONSE_TYPES_SUPPORTED
414420
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

oauth2_provider/handlers.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,84 @@
1+
import json
12
import logging
3+
from datetime import timedelta
24

5+
import requests
36
from django.contrib.auth.signals import user_logged_out
47
from django.dispatch import receiver
8+
from django.utils import timezone
9+
from jwcrypto import jwt
510

11+
from .exceptions import BackchannelLogoutRequestError
12+
from .models import AbstractApplication, get_id_token_model
613
from .settings import oauth2_settings
714

815

16+
IDToken = get_id_token_model()
17+
918
logger = logging.getLogger(__name__)
1019

1120

21+
def send_backchannel_logout_request(id_token, *args, **kwargs):
22+
"""
23+
Send a logout token to the applications backchannel logout uri
24+
"""
25+
26+
ttl = kwargs.get("ttl") or timedelta(minutes=10)
27+
28+
try:
29+
assert oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_ENABLED, "Backchannel logout not enabled"
30+
assert id_token.application.algorithm != AbstractApplication.NO_ALGORITHM, (
31+
"Application must provide signing algorithm"
32+
)
33+
assert id_token.application.backchannel_logout_uri is not None, (
34+
"URL for backchannel logout not provided by client"
35+
)
36+
37+
issued_at = timezone.now()
38+
expiration_date = issued_at + ttl
39+
40+
claims = {
41+
"iss": oauth2_settings.OIDC_ISS_ENDPOINT,
42+
"sub": str(id_token.user.id),
43+
"aud": str(id_token.application.client_id),
44+
"iat": int(issued_at.timestamp()),
45+
"exp": int(expiration_date.timestamp()),
46+
"jti": id_token.jti,
47+
"events": {"http://schemas.openid.net/event/backchannel-logout": {}},
48+
}
49+
50+
# Standard JWT header
51+
header = {"typ": "logout+jwt", "alg": id_token.application.algorithm}
52+
53+
# RS256 consumers expect a kid in the header for verifying the token
54+
if id_token.application.algorithm == AbstractApplication.RS256_ALGORITHM:
55+
header["kid"] = id_token.application.jwk_key.thumbprint()
56+
57+
token = jwt.JWT(
58+
header=json.dumps(header, default=str),
59+
claims=json.dumps(claims, default=str),
60+
)
61+
62+
token.make_signed_token(id_token.application.jwk_key)
63+
64+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
65+
data = {"logout_token": token.serialize()}
66+
response = requests.post(id_token.application.backchannel_logout_uri, headers=headers, data=data)
67+
response.raise_for_status()
68+
except (AssertionError, requests.RequestException) as exc:
69+
raise BackchannelLogoutRequestError(str(exc))
70+
71+
1272
@receiver(user_logged_out)
1373
def on_user_logged_out_maybe_send_backchannel_logout(sender, **kwargs):
1474
handler = oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_HANDLER
1575
if not oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_ENABLED or not callable(handler):
1676
return
1777

18-
handler(user=kwargs["user"])
78+
user = kwargs["user"]
79+
id_tokens = IDToken.objects.filter(application__backchannel_logout_uri__isnull=False, user=user)
80+
for id_token in id_tokens:
81+
try:
82+
handler(id_token=id_token)
83+
except BackchannelLogoutRequestError as exc:
84+
logger.warn(str(exc))

oauth2_provider/models.py

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import hashlib
2-
import json
32
import logging
43
import time
54
import uuid
65
from contextlib import suppress
76
from datetime import timedelta
87
from urllib.parse import parse_qsl, urlparse
98

10-
import requests
119
from django.apps import apps
1210
from django.conf import settings
1311
from django.contrib.auth.hashers import identify_hasher, make_password
@@ -16,11 +14,10 @@
1614
from django.urls import reverse
1715
from django.utils import timezone
1816
from django.utils.translation import gettext_lazy as _
19-
from jwcrypto import jwk, jwt
17+
from jwcrypto import jwk
2018
from jwcrypto.common import base64url_encode
2119
from oauthlib.oauth2.rfc6749 import errors
2220

23-
from .exceptions import BackchannelLogoutRequestError
2421
from .generators import generate_client_id, generate_client_secret
2522
from .scopes import get_scopes_backend
2623
from .settings import oauth2_settings
@@ -636,53 +633,6 @@ def revoke(self):
636633
"""
637634
self.delete()
638635

639-
def send_backchannel_logout_request(self, ttl=timedelta(minutes=10)):
640-
"""
641-
Send a logout token to the applications backchannel logout uri
642-
"""
643-
try:
644-
assert oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_ENABLED, "Backchannel logout not enabled"
645-
assert self.application.algorithm != AbstractApplication.NO_ALGORITHM, (
646-
"Application must provide signing algorithm"
647-
)
648-
assert self.application.backchannel_logout_uri is not None, (
649-
"URL for backchannel logout not provided by client"
650-
)
651-
652-
issued_at = timezone.now()
653-
expiration_date = issued_at + ttl
654-
655-
claims = {
656-
"iss": oauth2_settings.OIDC_ISS_ENDPOINT,
657-
"sub": str(self.user.id),
658-
"aud": str(self.application.client_id),
659-
"iat": int(issued_at.timestamp()),
660-
"exp": int(expiration_date.timestamp()),
661-
"jti": self.jti,
662-
"events": {"http://schemas.openid.net/event/backchannel-logout": {}},
663-
}
664-
665-
# Standard JWT header
666-
header = {"typ": "logout+jwt", "alg": self.application.algorithm}
667-
668-
# RS256 consumers expect a kid in the header for verifying the token
669-
if self.application.algorithm == AbstractApplication.RS256_ALGORITHM:
670-
header["kid"] = self.application.jwk_key.thumbprint()
671-
672-
token = jwt.JWT(
673-
header=json.dumps(header, default=str),
674-
claims=json.dumps(claims, default=str),
675-
)
676-
677-
token.make_signed_token(self.application.jwk_key)
678-
679-
headers = {"Content-Type": "application/x-www-form-urlencoded"}
680-
data = {"logout_token": token.serialize()}
681-
response = requests.post(self.application.backchannel_logout_uri, headers=headers, data=data)
682-
response.raise_for_status()
683-
except (AssertionError, requests.RequestException) as exc:
684-
raise BackchannelLogoutRequestError(str(exc))
685-
686636
@property
687637
def scopes(self):
688638
"""
@@ -913,15 +863,3 @@ def is_origin_allowed(origin, allowed_origins):
913863
return True
914864

915865
return False
916-
917-
918-
def send_backchannel_logout_requests(user):
919-
"""
920-
Creates logout tokens for all id tokens associated with the user
921-
"""
922-
id_tokens = IDToken.objects.filter(application__backchannel_logout_uri__isnull=False, user=user)
923-
for id_token in id_tokens:
924-
try:
925-
id_token.send_backchannel_logout_request()
926-
except BackchannelLogoutRequestError as exc:
927-
logger.warn(str(exc))

oauth2_provider/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"client_secret_basic",
9494
],
9595
"OIDC_BACKCHANNEL_LOGOUT_ENABLED": False,
96-
"OIDC_BACKCHANNEL_LOGOUT_HANDLER": "oauth2_provider.models.send_backchannel_logout_requests",
96+
"OIDC_BACKCHANNEL_LOGOUT_HANDLER": "oauth2_provider.handlers.send_backchannel_logout_request",
9797
"OIDC_RP_INITIATED_LOGOUT_ENABLED": False,
9898
"OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT": True,
9999
"OIDC_RP_INITIATED_LOGOUT_STRICT_REDIRECT_URIS": False,

tests/test_backchannel_logout.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from django.urls import reverse
99

1010
from oauth2_provider.exceptions import BackchannelLogoutRequestError
11-
from oauth2_provider.models import get_application_model, get_id_token_model, send_backchannel_logout_requests
11+
from oauth2_provider.models import get_application_model, get_id_token_model
12+
from oauth2_provider.handlers import (
13+
on_user_logged_out_maybe_send_backchannel_logout,
14+
send_backchannel_logout_request,
15+
)
1216
from oauth2_provider.views import ApplicationRegistration
1317

1418
from . import presets
@@ -42,12 +46,12 @@ def setUp(self):
4246
)
4347

4448
def test_on_logout_handler_is_called_for_user(self):
45-
with patch("oauth2_provider.models.send_backchannel_logout_requests") as backchannel_handler:
49+
with patch("oauth2_provider.handlers.send_backchannel_logout_request") as backchannel_handler:
4650
self.client.login(username="app_user", password="654321")
4751
self.client.logout()
4852
backchannel_handler.assert_called_once()
49-
args, kwargs = backchannel_handler.call_args
50-
self.assertEqual(kwargs.get("user"), self.user)
53+
_, kwargs = backchannel_handler.call_args
54+
self.assertEqual(kwargs["id_token"], self.id_token)
5155

5256
def test_logout_token_is_signed_for_user(self):
5357
with patch("requests.post") as mocked_post:
@@ -59,7 +63,7 @@ def test_raises_exception_on_bad_application(self):
5963
self.application.algorithm = Application.NO_ALGORITHM
6064
self.application.save()
6165
with self.assertRaises(BackchannelLogoutRequestError):
62-
self.id_token.send_backchannel_logout_request()
66+
send_backchannel_logout_request(self.id_token)
6367

6468
def test_new_application_form_has_backchannel_logout_field(self):
6569
factory = RequestFactory()
@@ -71,6 +75,6 @@ def test_new_application_form_has_backchannel_logout_field(self):
7175
self.assertTrue("backchannel_logout_uri" in form.fields.keys())
7276

7377
def test_logout_sender_does_not_crash_on_backchannel_error(self):
74-
with patch.object(self.id_token, "send_backchannel_logout_request") as mock_func:
78+
with patch("oauth2_provider.handlers.send_backchannel_logout_request") as mock_func:
7579
mock_func.side_effect = BackchannelLogoutRequestError("Bad Gateway")
76-
send_backchannel_logout_requests(self.user)
80+
on_user_logged_out_maybe_send_backchannel_logout(sender=User, user=self.user)

0 commit comments

Comments
 (0)