12
12
from django .conf import settings
13
13
from django .contrib .auth .hashers import identify_hasher , make_password
14
14
from django .core .exceptions import ImproperlyConfigured
15
- from django .db import IntegrityError , models , router , transaction
15
+ from django .db import models , router , transaction
16
16
from django .urls import reverse
17
17
from django .utils import timezone
18
18
from django .utils .translation import gettext_lazy as _
@@ -636,6 +636,48 @@ def revoke(self):
636
636
"""
637
637
self .delete ()
638
638
639
+ def send_backchannel_logout_request (self , ttl = timedelta (minutes = 10 )):
640
+ """
641
+ Send a token to
642
+ """
643
+ try :
644
+ assert oauth2_settings .OIDC_BACKCHANNEL_LOGOUT_ENABLED , "Backchannel logout is not enabled"
645
+ assert self .application .backchannel_logout_url is not None , (
646
+ "URL for backchannel logout not provided by client"
647
+ )
648
+
649
+ issued_at = timezone .now ()
650
+ expiration_date = issued_at + ttl
651
+
652
+ claims = {
653
+ "iss" : oauth2_settings .OIDC_ISS_ENDPOINT ,
654
+ "sub" : str (self .user .id ),
655
+ "aud" : str (self .application .client_id ),
656
+ "iat" : int (issued_at .timestamp ()),
657
+ "exp" : int (expiration_date .timestamp ()),
658
+ "jti" : self .jti ,
659
+ "events" : {"http://schemas.openid.net/event/backchannel-logout" : {}},
660
+ }
661
+
662
+ # Standard JWT header
663
+ header = {"typ" : "logout+jwt" , "alg" : self .application .algorithm }
664
+ # RS256 consumers expect a kid in the header for verifying the token
665
+ if self .application .algorithm == AbstractApplication .RS256_ALGORITHM :
666
+ header ["kid" ] = self .application .jwk_key .thumbprint ()
667
+
668
+ token = jwt .JWT (
669
+ header = json .dumps (header , default = str ),
670
+ claims = json .dumps (claims , default = str ),
671
+ )
672
+ token .make_signed_token (self .application .jwk_key )
673
+
674
+ headers = {"Content-Type" : "application/x-www-form-urlencoded" }
675
+ data = {"logout_token" : token .serialize ()}
676
+ response = requests .post (self .application .backchannel_logout_uri , headers = headers , data = data )
677
+ response .raise_for_status ()
678
+ except (AssertionError , requests .RequestException ) as exc :
679
+ raise BackchannelLogoutRequestError (str (exc ))
680
+
639
681
@property
640
682
def scopes (self ):
641
683
"""
@@ -657,71 +699,6 @@ class Meta(AbstractIDToken.Meta):
657
699
swappable = "OAUTH2_PROVIDER_ID_TOKEN_MODEL"
658
700
659
701
660
- class AbstractLogoutToken (models .Model ):
661
- TTL = timedelta (minutes = 10 )
662
-
663
- id = models .BigAutoField (primary_key = True )
664
- user = models .ForeignKey (
665
- settings .AUTH_USER_MODEL , on_delete = models .CASCADE , related_name = "%(app_label)s_%(class)s"
666
- )
667
- application = models .ForeignKey (oauth2_settings .APPLICATION_MODEL , on_delete = models .CASCADE )
668
- id_token = models .OneToOneField (oauth2_settings .ID_TOKEN_MODEL , null = True , on_delete = models .SET_NULL )
669
- created = models .DateTimeField (auto_now_add = True )
670
-
671
- @property
672
- def expires (self ):
673
- return self .created + self .TTL
674
-
675
- @property
676
- def jwt_claim_dictionary (self ):
677
- claims = {
678
- "iss" : oauth2_settings .OIDC_ISS_ENDPOINT ,
679
- "sub" : str (self .user .id ),
680
- "aud" : str (self .application .client_id ),
681
- "iat" : int (self .created .timestamp ()),
682
- "exp" : int (self .expires .timestamp ()),
683
- "events" : {"http://schemas.openid.net/event/backchannel-logout" : {}},
684
- }
685
-
686
- if self .id_token and self .id_token .jti :
687
- claims ["jti" ] = str (self .id_token .jti )
688
-
689
- return claims
690
-
691
- def get_serialized_jwt (self ):
692
- # Standard JWT header
693
- header = {"typ" : "logout+jwt" , "alg" : self .application .algorithm }
694
- # RS256 consumers expect a kid in the header for verifying the token
695
- if self .application .algorithm == AbstractApplication .RS256_ALGORITHM :
696
- header ["kid" ] = self .application .jwk_key .thumbprint ()
697
-
698
- token = jwt .JWT (
699
- header = json .dumps (header , default = str ),
700
- claims = json .dumps (self .jwt_claim_dictionary , default = str ),
701
- )
702
- token .make_signed_token (self .application .jwk_key )
703
- return token .serialize ()
704
-
705
- def send_backchannel_logout_request (self ):
706
- if not oauth2_settings .OIDC_BACKCHANNEL_LOGOUT_ENABLED :
707
- return
708
- try :
709
- headers = {"Content-Type" : "application/x-www-form-urlencoded" }
710
- data = {"logout_token" : self .get_serialized_jwt ()}
711
- response = requests .post (self .application .backchannel_logout_uri , headers = headers , data = data )
712
- response .raise_for_status ()
713
- except (AssertionError , requests .RequestException ) as exc :
714
- raise BackchannelLogoutRequestError (str (exc ))
715
-
716
- class Meta :
717
- abstract = True
718
-
719
-
720
- class LogoutToken (AbstractLogoutToken ):
721
- class Meta (AbstractLogoutToken .Meta ):
722
- swappable = "OAUTH2_PROVIDER_LOGOUT_TOKEN_MODEL"
723
-
724
-
725
702
def get_application_model ():
726
703
"""Return the Application model that is active in this project."""
727
704
return apps .get_model (oauth2_settings .APPLICATION_MODEL )
@@ -742,11 +719,6 @@ def get_id_token_model():
742
719
return apps .get_model (oauth2_settings .ID_TOKEN_MODEL )
743
720
744
721
745
- def get_logout_token_model ():
746
- """Return the IDToken model that is active in this project."""
747
- return apps .get_model (oauth2_settings .LOGOUT_TOKEN_MODEL )
748
-
749
-
750
722
def get_refresh_token_model ():
751
723
"""Return the RefreshToken model that is active in this project."""
752
724
return apps .get_model (oauth2_settings .REFRESH_TOKEN_MODEL )
@@ -776,12 +748,6 @@ def get_id_token_admin_class():
776
748
return id_token_admin_class
777
749
778
750
779
- def get_logout_token_admin_class ():
780
- """Return the LogoutToken admin class that is active in this project."""
781
- logout_token_admin_class = oauth2_settings .LOGOUT_TOKEN_ADMIN_CLASS
782
- return logout_token_admin_class
783
-
784
-
785
751
def get_refresh_token_admin_class ():
786
752
"""Return the RefreshToken admin class that is active in this project."""
787
753
refresh_token_admin_class = oauth2_settings .REFRESH_TOKEN_ADMIN_CLASS
@@ -812,7 +778,6 @@ def batch_delete(queryset, query):
812
778
access_token_model = get_access_token_model ()
813
779
refresh_token_model = get_refresh_token_model ()
814
780
id_token_model = get_id_token_model ()
815
- logout_token_model = get_logout_token_model ()
816
781
grant_model = get_grant_model ()
817
782
REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings .REFRESH_TOKEN_EXPIRE_SECONDS
818
783
@@ -840,11 +805,6 @@ def batch_delete(queryset, query):
840
805
else :
841
806
logger .info ("refresh_expire_at is %s. No refresh tokens deleted." , refresh_expire_at )
842
807
843
- logout_token_query = models .Q (created__lt = now - logout_token_model .TTL )
844
- logout_tokens = logout_token_model .objects .filter (logout_token_query )
845
- logout_tokens_delete_no = batch_delete (logout_tokens , logout_token_query )
846
- logger .info ("%s Expired logout tokens deleted" , logout_tokens_delete_no )
847
-
848
808
access_token_query = models .Q (refresh_token__isnull = True , expires__lt = now )
849
809
access_tokens = access_token_model .objects .filter (access_token_query )
850
810
@@ -957,11 +917,6 @@ def send_backchannel_logout_requests(user):
957
917
id_tokens = IDToken .objects .filter (application__backchannel_logout_uri__isnull = False , user = user )
958
918
for id_token in id_tokens :
959
919
try :
960
- logout_token = LogoutToken .objects .create (
961
- user = user , application = id_token .application , id_token = id_token
962
- )
963
- logout_token .send_backchannel_logout_request ()
920
+ id_token .send_backchannel_logout_request ()
964
921
except BackchannelLogoutRequestError as exc :
965
922
logger .warn (str (exc ))
966
- except IntegrityError :
967
- logger .warn (f"Logout for { id_token } already exists" )
0 commit comments