Skip to content

Commit 4aeac81

Browse files
committed
Backchannel Logout
- Add migration to add backchannel logout support - Add model for Logout Token - Add admin for Logout Token - Add parameters related to backchannel logout on OIDC Discovery View - Change application creation and update form - Change template that renders information about the application -
1 parent 8d3e7a9 commit 4aeac81

18 files changed

+329
-45
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Peter Karman
9999
Peter McDonald
100100
Petr Dlouhý
101101
pySilver
102+
Raphael Lullis
102103
Rodney Richardson
103104
Rustem Saiargaliev
104105
Rustem Saiargaliev

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77

88
## [unreleased]
9+
910
### Added
1011
* #1506 Support for Wildcard Origin and Redirect URIs
12+
* #1545 Support for OIDC Back-Channel Logout
1113
<!--
1214
### Changed
1315
### Deprecated

docs/oidc.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ This feature has to be enabled separately as it is an extension to the core stan
169169
}
170170
171171
172+
Backchannel Logout Support
173+
~~~~~~~~~~~~~~~~~~~~~~~~~~
174+
175+
`Backchannel Logout`_ is an extension to the core standard which
176+
allows the OP to send direct requests to terminate sessions at the RP.
177+
178+
.. code-block:: python
179+
180+
OAUTH2_PROVIDER = {
181+
# OIDC has to be enabled to use Backchannel logout
182+
"OIDC_ENABLED": True,
183+
"OIDC_ISS_ENDPOINT": "https://idp.example.com", # Required for issuing logout tokens
184+
# Enable and configure Backchannel Logout Support
185+
"OIDC_BACKCHANNEL_LOGOUT_ENABLED": True,
186+
# ... any other settings you want
187+
}
188+
189+
.. _Backchannel Logout: https://openid.net/specs/openid-connect-backchannel-1_0.html
190+
191+
To make use of this, the application being created needs to provide a
192+
valid `backchannel_logout_endpoint`.
193+
194+
172195
Setting up OIDC enabled clients
173196
===============================
174197

docs/settings.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ When is set to ``False`` (default) the `OpenID Connect RP-Initiated Logout <http
361361
endpoint is not enabled. OpenID Connect RP-Initiated Logout enables an :term:`Client` (Relying Party)
362362
to request that a :term:`Resource Owner` (End User) is logged out at the :term:`Authorization Server` (OpenID Provider).
363363

364+
364365
OIDC_RP_INITIATED_LOGOUT_ALWAYS_PROMPT
365366
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
366367
Default: ``True``
@@ -400,6 +401,15 @@ discovery metadata from ``OIDC_ISS_ENDPOINT`` +
400401
If unset, the default location is used, eg if ``django-oauth-toolkit`` is
401402
mounted at ``/o``, it will be ``<server-address>/o``.
402403

404+
OIDC_BACKCHANNEL_LOGOUT_ENABLED
405+
~~~~~~~~~~~~~~~~~~~~~~~~
406+
Default: ``False``
407+
408+
When is set to ``False`` (default) the `OpenID Connect Backchannel Logout <https://openid.net/specs/openid-connect-backchannel-1_0.html>`_
409+
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)
410+
indicating that a session from the :term:`Resource Owner` (End User) has ended.
411+
412+
403413
OIDC_RESPONSE_TYPES_SUPPORTED
404414
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
405415
Default::

oauth2_provider/admin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
get_grant_model,
1111
get_id_token_admin_class,
1212
get_id_token_model,
13+
get_logout_token_model,
14+
get_logout_token_admin_class,
1315
get_refresh_token_admin_class,
1416
get_refresh_token_model,
1517
)
@@ -51,6 +53,14 @@ class IDTokenAdmin(admin.ModelAdmin):
5153
list_select_related = ("application", "user")
5254

5355

56+
class LogoutTokenAdmin(admin.ModelAdmin):
57+
list_display = ("user", "application", "id_token")
58+
raw_id_fields = ("user",)
59+
search_fields = ("user__email",) if has_email else ()
60+
list_filter = ("application",)
61+
list_select_related = ("application", "user", "id_token")
62+
63+
5464
class RefreshTokenAdmin(admin.ModelAdmin):
5565
list_display = ("token", "user", "application")
5666
raw_id_fields = ("user", "access_token")
@@ -62,16 +72,19 @@ class RefreshTokenAdmin(admin.ModelAdmin):
6272
access_token_model = get_access_token_model()
6373
grant_model = get_grant_model()
6474
id_token_model = get_id_token_model()
75+
logout_token_model = get_logout_token_model()
6576
refresh_token_model = get_refresh_token_model()
6677

6778
application_admin_class = get_application_admin_class()
6879
access_token_admin_class = get_access_token_admin_class()
6980
grant_admin_class = get_grant_admin_class()
7081
id_token_admin_class = get_id_token_admin_class()
82+
logout_token_admin_class = get_logout_token_admin_class()
7183
refresh_token_admin_class = get_refresh_token_admin_class()
7284

7385
admin.site.register(application_model, application_admin_class)
7486
admin.site.register(access_token_model, access_token_admin_class)
7587
admin.site.register(grant_model, grant_admin_class)
7688
admin.site.register(id_token_model, id_token_admin_class)
89+
admin.site.register(logout_token_model, logout_token_admin_class)
7790
admin.site.register(refresh_token_model, refresh_token_admin_class)

oauth2_provider/apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ class DOTConfig(AppConfig):
88
def ready(self):
99
# Import checks to ensure they run.
1010
from . import checks # noqa: F401
11+
from . import handlers # noqa

oauth2_provider/checks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def validate_token_configuration(app_configs, **kwargs):
1313
oauth2_settings.ACCESS_TOKEN_MODEL,
1414
oauth2_settings.ID_TOKEN_MODEL,
1515
oauth2_settings.REFRESH_TOKEN_MODEL,
16+
oauth2_settings.LOGOUT_TOKEN_MODEL,
1617
)
1718
)
1819

@@ -26,3 +27,16 @@ def validate_token_configuration(app_configs, **kwargs):
2627
return [checks.Error("The token models are expected to be stored in the same database.")]
2728

2829
return []
30+
31+
32+
@checks.register()
33+
def validate_backchannel_logout(app_configs, **kwargs):
34+
errors = []
35+
36+
if oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_ENABLED:
37+
if not callable(oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_HANDLER):
38+
errors.append(checks.Error("OIDC_BACKCHANNEL_LOGOUT_HANDLER must be a callable."))
39+
if not oauth2_settings.OIDC_ISS_ENDPOINT:
40+
errors.append(checks.Error("OIDC_ISS_ENDPOINT must be set to enable OIDC backchannel logout."))
41+
42+
return errors

oauth2_provider/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ def __init__(self, description=None):
3535
super().__init__(message)
3636

3737

38+
class BackchannelLogoutRequestError(OIDCError):
39+
error = "backchannel_logout_request_failed"
40+
41+
3842
class InvalidRequestFatalError(OIDCError):
3943
"""
4044
For fatal errors. These are requests with invalid parameter values, missing parameters or otherwise

oauth2_provider/handlers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import logging
2+
3+
from django.contrib.auth.signals import user_logged_out
4+
from django.dispatch import receiver
5+
6+
from .settings import oauth2_settings
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
@receiver(user_logged_out)
12+
def on_user_logged_out_maybe_send_backchannel_logout(sender, **kwargs):
13+
handler = oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_HANDLER
14+
if not oauth2_settings.OIDC_BACKCHANNEL_LOGOUT_ENABLED or not callable(handler):
15+
return
16+
17+
handler(user=kwargs["user"])
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 5.2 on 2025-06-02 18:31
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('oauth2_provider', '0012_add_token_checksum'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.AddField(
17+
model_name='application',
18+
name='backchannel_logout_uri',
19+
field=models.URLField(blank=True, help_text='Backchannel Logout URI where logout tokens will be sent', null=True),
20+
),
21+
migrations.CreateModel(
22+
name='LogoutToken',
23+
fields=[
24+
('id', models.BigAutoField(primary_key=True, serialize=False)),
25+
('created', models.DateTimeField(auto_now_add=True)),
26+
('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
27+
('id_token', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.OAUTH2_PROVIDER_ID_TOKEN_MODEL)),
28+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s', to=settings.AUTH_USER_MODEL)),
29+
],
30+
options={
31+
'abstract': False,
32+
'swappable': 'OAUTH2_PROVIDER_LOGOUT_TOKEN_MODEL',
33+
},
34+
),
35+
]

0 commit comments

Comments
 (0)