Package: django-sso
Django SSO (Single Sign-On) [Beta]Implementation of Single Sign-On for Django.Supported Django versions:3.*4.*It's recommended to force specify package versions while installing packages. This is for security and compatibility reasons.How does it work?You should install this library to your Django project, that you plan to use as the authorization gateway (this instance will keep all user accounts). Then add this library to services which you wanna authenticate via the SSO gateway, add a little bit of configuration and that's all!InstallationServer sideAdd the module nameddjango_sso.gatewayto theINSTALLED_APPSvariable# project/settings.pyINSTALLED_APPS=[# ...'django_sso.sso_gateway',]Migrate the gateway models./manage.pymigratesso_gatewayAdd sso-urls to the project:# project/urls.pyurlpatterns=[# ...,path('',include('django_sso.sso_gateway.urls')),]In the admin panel you will see a new section namedSINGLE SIGN-ON. And inSubordinated servicesyou should create a new entry with:Name- Human name of the serviceBase url- URL for redirects and access to service endpoints from the server. (Likehttps://your-service.example).Enabled- Are subordinated services active. (Inactive services can’t communicate with the server and the server can’t communicate with it)Token- Automatically generated token you should paste into your servicessettings.py. Copy this for the next step of configuring the client.Settings (if needed)# SSO settings section in the gateway side are optionalSSO={# Timeout for the communication with subordinated services. (OPTIONAL)# This timeout is defined in seconds with a default value of 0.1s# (100ms) per registered service.'SUBORDINATE_COMMUNICATION_TIMEOUT':0.1,# Additional fields. (OPTIONAL). For more details look to part# named as "Send additional data to subordinated services"'ADDITIONAL_FIELDS':('additiona_fields','from_user_model','and_related_models'),}# Affects to the "welcome" url (after successful authentication) when# user logged in but don't have url to redirect. Optional.# Compatible logic with Django.LOGIN_REDIRECT_URL='/default/url/'Then server side is ready to use!Client sideWhen the library is attached to the client side project, the admin login form will be overridden with same view aslogin/at client side.Adddjango_sso.sso_servicetoINSTALLED_APPS# project/settings.pyINSTALLED_APPS=[# ...'django_sso.sso_service',]Add urls to service application# project/urls.pyurlpatterns=[# ...,path('',include('django_sso.sso_service.urls')),]Setup settings variables# project/settings.py# Django variable. URL for unlogged users. We redirect it to our view.LOGIN_URL='/login/'SSO={# Specify SSO server base url (REQUIRED)'ROOT':'https://sso.project.test',# Specify application token obtained in the SSO server admin panel (REQUIRED)'TOKEN':'reej8Vt5kbCPJM9mZQqYsvfxC...',# Overriding event acceptor class (OPTIONAL). For more details read# "Overriding event acceptor in subordinated service" partition'EVENT_ACCEPTOR_CLASS':'project.my_overrides.MySSOEventAcceptor'}Default behavior summary (In case, when the SSO gateway and subordinated service implemented with the Django SSO)BehaviorWhen user created/updated on the gateway app - system will emit to all subbordinated services profile info (creates or updates profile data with full list of basic and additional fields.When direct related model (provided in ADDITIONAL_FIELDS setting) created or changed - system will emit event to all services with information about changes. By default - additional fields updation - on developer hands (you must override EventAcceptor class and provide own actions for events. You can read about it below). When deleted - system will send nulls same as sends data to all new/old related users.When the user deauthenticated the Django SSO package will emit deauthentication event and instantly purge the sessions on subbordinated services.When user deleted on the gateway side - the gateway app will emit event and in subordinated services user instantly will disabled if providedis_activeproperty. By Django authentication subystem rules - if user haveis_active=False- system will not authenticate it.Recomendations and importantsIn your project gateway and all subordinated services must have same major version(MAJOR.MINOR.CHORE_OR_FIX). Also very recomendet to install package with fixed version. Autoupdates may brokes your because breaking changes.Youmustprevent overriding all data, wich coming from the SSO gateway app. You can change additional fields of the your user model in subbordinated service, but fields, provided from SSO exchange must be overriden only by the SSO gateway events.StructureServer side URLslogin/- central login form (you can override templatedjango_sso/login.html)logout/- central logout view. Clear all sessions on all resources for userInternal library urls (endpoints for services):sso/obtain/- obtain authorization requestsso/get/- get SSO token information. (Is authorized for this token? Get user identity from token. etc..)sso/make_used/- after successful authentication on client side need to mark authorization request as used.sso/deauthenticate/- services sends deauthentication requests to SSO-server. SSO server broadcasts all services to deauthenticate usersso/debug/update_event/- View for debugging theSSO[ADDITIONAL_FIELDS]setting. This URL enabled only in the DjangoDEBUGmode only. Here you can see emitted variables to all subordinated services per every login or affected model updation/deletion/creation.__welcome/- Sample view for testing. For logged and unlogged users.Client side urlslogin/- login form. Intermediate form. Obtains authentication request and redirects to SSO server/login.logout/- Deauthenticates the user and emits deauthentication event to SSO-server (tosso/deauthenticate/at server side).sso/test/- Page for testing the SSO mechanism immediately after installation ofdjango_sso. When you open it in browser: If user are logged in - shows the username or redirects to SSO server and comes back after successful authentication.Library urls for internal usage (endpoints for SSO-server side)sso/event/- Event acceptor from SSO server. Look at “SSO with non-Django project / Accepting events” sectionsso/accept/- User after successful authentication comes back. SSO-server redirect it to this URL for make Django authorization. Then after session is up - browser will redirect to the next URL.OverridingUser and session storageThis library based on Django user subsystem and Django session subsystem. Also supports custom classes, but he must be based on classical Django classes (AbstractUser / AbstractBaseUser, etc..). This means that you have two ways. One: Do nothing, just install library to gateway/services and just use it. Two: Just pick own classes of user/session with based Django`s mechanisms.Overriding event acceptor in subordinated serviceFor event processing you must declare own class and inherit it from base class located indjango_sso.sso_service.backends.EventAcceptor. Inheritance are necessary. Arguments must  absolutely matches for overridden methods.# project/my_overrides.pyfromdjango_sso.sso_service.backendsimportEventAcceptor,acceptor# In case, when you need to do something after deauthenticationclassMyEventAcceptor(EventAcceptor):@acceptor# Every event accpetor method must be decorated with itdefdeauthenticate(self,username):# Here you can do own actions before deauthenticationsuper().deauthenticate(username)# And here you can do own actions after deauthentication# In other case, when you need to override default behavior of classclassMyHardEventAcceptor(EventAcceptor):@acceptordefdeauthenticate(self,username):# Here you do own actionsMethod names are the same that event types. Seehere.Then next put the path to this class intosettings.py:SSO={# Your settings...'EVENT_ACCEPTOR_CLASS':'project.my_overrides.MySSOEventAcceptor'}Methods of theEventAcceptorclass are the same that event names provided in scheme.Send additional data to subordinated servicesBy default the SSO gateway sends to subordinated services next fields (if provided in user model):
Also, this names are reserved and overriding of it is restricted in the ADDITIONAL_FIELDS setting.is_staff- Are user is staff member (can access admin panel).is_active- Obviously, activity status.is_superuser- Are user have full privileges.user_identy- User identity. Obtained fromYourUserModel.USERNAME_FIELD. Unique and not null ever. E-Mail / Login etc.Additional fields are not included in the default behavior for security reasons and for clarity of the project code and the code behavior.But possible cases when you need to send more data, than only this flags. For this case the library have next way:Imagine, that we have next:# Anything additional dataclassCategory(models.Model):name=models.CharField(max_length=12)defto_sso_representation(self):"""This method will call if this instance in the ADDITIONAL_FIELDS provided just as a variable"""returnf'to_sso_representation() for Category #{self.name}'@propertydefmy_var(self):returnf'my_var for user #{self.id}'defmy_method(self):"""Your method that returns data (should be too fast)- it is recommended to mark this method for security reasons, as it is used in the SSO mechanism- recommended to be named like "sso_field__my_id_is" for code style and obviousness"""returnf'my_method() for user #{self.id}'# One2One reverse related model wich extends user modelclassReversedAdvancedUserData(models.Model):something_else=CharField(max_length=12,default=None,null=True)# Custom user model or the default auth.User model (no difference)classUserModel(AbstractUser):my_custom_field=models.CharField(max_length=16,default=None,null=True)category=models.ForeignKey(Category,default=None,null=True,on_delete=models.CASCADE)advanced_data2=models.OneToOneField(ReversedAdvancedUserData,null=True,on_delete=models.SET_NULL,related_name='data2')# The One2One related model which extends user model (possible that you create this instance via signals)classAdvancedUserData(models.Model):user=models.OneToOneField(UserModel,on_delete=models.CASCADE,related_name='data')my_data_field=models.IntegerField(default=1)On the gateway side insettings.pyadd variable with implementation like next:SSO={# Additional fields wich send with update user event (Any fields related to user model)# This setting are optional'ADDITIONAL_FIELDS':(# It's a UserModel.my_custom_field'my_custom_field',# It's a Category.name from UserModel.category'category.name',# Will put Category.to_sso_representation (or __str__) by UserModel.category object'category',# If prop isset - will put AdvancedUserData.my_data_field'data.my_data_field',# Will get value from the AdvancedUserData.to_sso_representation method# or AdvancedUserData.__str__'data',# Will get value from ReversedAdvancedUserData.to_sso_representation method# or ReversedAdvancedUserData.__str__'advanced_data2',# This field is UserModel.my_custom_field with an alias named "an_alias"'my_custom_field:an_alias',# Also you can put properties and methods to sending data'category.my_method',# Category.my_method will called'category.my_var',# Get the Category.my_var prop value)}​Value, that you put to fields, must beboolorstrorintorfloatorNone. If expected any other type: thedjango-ssolibrary will try to cast tostrorwill log error to console.When you will set theSSO_ADDITIONAL_FIELDSvariable - system will subscribe to allpre_savesignals of provided models and will get all described values and will emit event with it and list of users, related to changes. Event emits when any model has changed/deleted. If model deleted - Django SSO will emit fields filled with null to all related users to deleted object.All finaly translated (to aliases or not) field names must be distinct and not overrides the basic variables (is_staff, enabled, is_superuser) else you will catch exception on project start.Any field may be aliasedIf methods execution time greater than 200ms - you got a detailed warning in console.Property order flow (all properties are related to user model at begin):Allowed fields only from direct relations. Nested relations restricted.IfForeignKeyorOneToOneFieldrelation isNone-Nonewill placed to result.If during method execution occurs exception - to field will placedNoneand to console wil printed detailed warning.Also exists page for debugging fields (see the gateway urls for more details).Debug & development noticeAll exceptions in SSO mechanism will be logged in console.Note: The gateway and the services have to run on distinct domains to not mess with each others cookies. For development on a single machine use thehostsfile to create multiple domains like this:127.0.0.1	sso_gateway
127.0.0.1	my_sso_service_1
127.0.0.1	my_sso_service_2Otherwise, after successful authentication at the SSO gateway the system can't send the event to it's subordinated service. For upmost three different services you can also make use of localhost, 127.0.0.1 and 0.0.0.0 as distinct domain names. :)If an error occurs after the authorization at the gateway, the user will be redirected to/?sso_broken_token=trueat the subordinated service.In case the authorization was successful but an error is caught while emitting the event the user will be redirected to the gateway page with url/__welcome/fallback=true.SSO with non-Django projectBasicsAny external service must be registered in SSO server in admin panel section namedSINGLE SIGN-ON / Subordinated services. Then obtained token put to your script for next calls. And make service available directly for SSO server.In next examples i’ll use sso_server.test meaning SSO server.Login pageWhen unlogged user visits login page, the backends need to request the SSO token from the SSO server.Fields:token- obtained from first step at SSO servernext_url- relative URL for redirect after successful authentication. (SSO will generateBasic URL + Next URLstring and will forward user to it)curl--requestPOST\--urlhttp://sso_server.test/sso/obtain/\--header'Content-Type: application/x-www-form-urlencoded'\--header'X-Token: IJj42agKd231SzinVYqJMqq0buinM0wU'\--datatoken=AhTu1Un5zef3eRMGsL3y7AbDt2213123123f\--datanext_url=/successful/page/If all succeeds, server will send you an authentication request token in JSON format.{"token":"NmyWRItAye0gDxX7CZhOFs2HKZtT3xyfdrq14TU"}ThenWrite token to session. (In PHP - $_SESSION.)Redirect user to http://sso_server.test/login/?sso=NmyWRItAye0gDxX7CZhOFs2HKZtT3xyfdrq14TU. You should put the obtained token to URL into “sso” parameter. User will be redirected to SSO login page.On SSO login page next:If user successful logged on SSO, SSO sends to your event endpoint basic information about user. You should be it write to your authentication system. Then SSO server redirects user back to http://your_service.test/sso/accept/ where script recover SSO token from session and request information from SSO:token- Service token.authentication_token- SSO token, obtained in last step.curl--requestPOST\--urlhttp://sso_server.test/sso/get/\--header'Content-Type: application/x-www-form-urlencoded'\--header'X-Token: IJj42agKdp31SzinVYqJMqq0buinM0wU'\--datatoken=AhTu1Un5zef3eRMGsL3y7AbDt2213123123f\--dataauthentication_token=NmyWRItAye0gDxX7CZhOFs2HKZtT3xyfdrq14TUIf user already authorized. It will be returned next JSON:{"authenticated":True,// Are successful authenticated"user_identy":"somebody",// User identy (login or email...)"next_url":"/admin/"// URL for redirect after successful auth}In any other case:{"error":"Authentication request token doesn't exist"}If all success. You need to notify SSO server that token is used. Do next:token- Service token.authentication_token- SSO token, obtained in last step.curl--requestPOST\--urlhttp://sso_server.test/sso/make_used/\--header'Content-Type: application/x-www-form-urlencoded'\--header'X-Token: IJj42agKdp31SzinVYqJMqq0buinM0wU'\--datatoken=AhTu1Un5zef3eRMGsL3y7AbDt2213123123f\--dataauthentication_token=NmyWRItAye0gDxX7CZhOFs2HKZtT3xyfdrq14TUAnd you will be get reply:{"ok":true}Logout pageYou first purge data from session. Then send to SSO server deauthentication event.token- Service token.user_identy- username or email field. Same, that you obtained from http://sso_server.test/sso/get/ at login procedure.curl--requestPOST\--urlhttp://127.0.0.1:5000/sso/deauthenticate/\--header'Content-Type: application/x-www-form-urlencoded'\--header'X-Token: IJj42agKdp31SzinVYqJMqq0buinM0wU'\--datatoken=AhTu1Un5zef3eRMGsL3y7AbDt2213123123f\--datauser_identy=someodyWill respond with{"ok":true}Meaning, that deauthentication completed for all services.Accepting eventsYou should create/sso/event/endpoint in your project.When user does successful authentication on SSO server, when user deleted or changed on SSO server, this library will send events to all subordinated services information about it. Account was deleted or marked as superuser or something other - SSO-server will emit events to all subordinated services.For example. When user is marked as superuser, SSO-server will cast event to all subordinated services tosso/event/. Next written all possible events.type,tokenfields in event data are permanent.Events sends in JSON format via POST request.Create/update accountWhen user is created, updated, disabled, (un)marked as superuser and with every login. Bydjango-ssobehavior: Event fieldsis_active,is_staff,is_superuserwill cast tobool.{"type":"update_account",// Event name"token":"kaIrVNHF4msyLBJeaD4hSO",// Service token to authenticate SSO server// Next fields may be not included in event. Because user model on SSO don't have it"fields":["user_identy":"somebody",// Value from username field"is_active":True,// Active user at SSO server"is_staff":True,// User is staff member"is_superuser":True,// User is superuser"custom_field":"Somethig from field",// Custom field"data.field":1,// Custom field from OneToOne related object or ForeginKey field if isset,]}DeauthenticateWhen user requested for deauthentication at any service or user deleted on the SSO gateway. This event will be emitted to all active subordinated services.{"type":"deauthenticate",// Event name"token":"kaIrVNHF4msyLBJeaD4hSO",// Service token to authenticate SSO server"user_identy":"somebody"// Value from username field}Delete accountWhen user has been deleted on the SSO gateway - system will emit event about it with next body:{"type":"delete",// Event name"token":"kaIrVNHF4msyLBJeaD4hSO",// Service token to authenticate SSO server"user_identy":"somebody"// value from username field}Change user identyWhen user has been renamed on SSO gateway - system will emit event with old and new identy to rename user on all subordinated services.{"type":"change_user_identy",// Event name"token":"kaIrVNHF4msyLBJeaD4hSO",// Service token to authenticate SSO server"old":"old_user@email_or.login",// Old identy"new":"new_user@email_or.login"// New identy}Additional fields updateIf you set anadditional fieldsin the settings (SSO['ADDITIONAL_FIELDS']) and the fields are taken from models other than yourUserModel: The SSO Gateway will generate the next event for each model separately and cast it to all subordinated services:(following examples will be written in context ofimagintaion)//Incasewhencreatedordeleted//ForCategorymodelwillnext:{"type":"update_fields","token":"kaIrVNHF4msyLBJeaD4hSO","fields":["category.name":"New name","category.id":"New category id"],"user_identities":["admin","test","DAVIDhaker"]}//ForAdvancedUserDatanext:{"type":"update_fields","token":"kaIrVNHF4msyLBJeaD4hSO","fields":["data.my_data_field":"New value 2"],"user_identities":["admin","test","DAVIDhaker"]}//Ifrelatedobjectdeleted,in"fields"allvalueswillbenull.{"type":"fields_update","token":"kaIrVNHF4msyLBJeaD4hSO","fields":["data.my_data_field":null],"user_identities":["admin","test","DAVIDhaker"]}For all requests tosso/event/subordinated service must be return next reponces// In successful case{"ok":True}// Else if failed{"error":"Error description here"}To do and coming fixes | roadmapAutomatic test on all Django versions while push to master branchAccess control to subordinated services. Possibility to set available services per user.Partially realized via the additional fields.Event queue for pushing events instead of immediately pushing. For stability and efficiency andfor preventing of lost info, when subordinated service are off-line.Integration with popular frameworks and making plug-ins for popular languages. (I can accept your code as part of project - link to repository, for example.)Integrate with SentryMultilanguage docsLibrary provided mechanism for management user permissions.NotesAll new features from PR/ISSUES will be pushed tonextbranch first, then after merge to master will be published to thePyPI.SupportThere are more plans to make it better... Also i wanna to translate it to popular languages.If you wanna to support project. You can do it viaEthereum: 0x2BD7aA911861029feB08430EEB9a36DC9a8A14d2 (also accept any token :-) )BUSD/BNB or any token (BEP20):  0x74e47ae3A26b8C5cD84d181595cC62723A1B114Eor any other way. You can mail ask about it.Any thoughts:me@davidhaker.ruWith love for open source!