My Django application has two types of users, each with their own authentication mechanism: regular users log in with a username and password, and employees log in via the company's SSO server using django-cas-client.
For this purpose I need two user tables: one for regular users and one for employees. A single table won't cut it, because the usernames might overlap. That is, usernames are unique for regular users, and are unique for employees, but a regular user and an employee might have the same username.
Similar questions like this one, this one and this one don't apply to my situation, because in my case the different user types can have the same username, while still being different users. Unfortunately, I have no influence over the usernames that are returned by the CAS server.
This is what I've got so far:
Instead of creating two user models, I created two profile models, linked via a OneToOneField to the user model:
from django.conf import settings
class RegularUser(models.Model)
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='regular_user')
nickname = models.CharField(max_length=255, blank=True)
class Employee(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='employee')
first_name = models.CharField(max_length=255, blank=True)
full_name = models.CharField(max_length=255, blank=True)
NR = models.CharField(max_length=255, blank=True)
email = models.CharField(max_length=255, blank=True)
I then used a django-cas-client CAS Response Callback to automatically populate newly created CAS users with the employee_number retrieved via LDAP:
from django.contrib.auth import get_user_model
from .models import Employee
def callback(tree):
username = tree[0][0].text
user, created = get_user_model().objects.get_or_create(username=username)
employee, created = Employee.objects.get_or_create(user=user)
try:
(
employee.first_name,
employee.full_name,
employee.NR,
employee.email
) = search_ldap(username)
employee.save()
except LDAPError:
pass
For those interested, this is the implementation of the search_ldap()
function:
def search_ldap(username):
result = ()
baseDN = "o=Our Organization Name,c=NL"
searchFilter = '(uid={})'.format(username)
attributes = ['givenName', 'cn', 'employeeNumber', 'mail']
try:
server = Server('ldap.example.com', use_ssl=True)
conn = Connection(server, auto_bind=True)
conn.search(baseDN, searchFilter, attributes=attributes)
for a in attributes:
result += (conn.response[0]['attributes'][a][0], )
except Exception:
raise LDAPError('Error in LDAP query')
return result
The end result is that only users that have logged in via the CAS server possess the employee
attribute. I haven't got around to automatically creating regular users, but of course only they will possess the regular_user
attribute. To log users in, I have defined two separate login pages in my urls.py
:
import cas.views
import django.contrib.auth.views
from django.conf.urls import include, url
from .views import login, logout
urlpatterns = [
url(r'^login/regular/$', django.contrib.auth.views.login, name='login_regular'),
url(r'^logout/regular/$', django.contrib.auth.views.logout, name='logout_regular'),
url(r'^login/sso/$',cas.views.login, name='login_sso'),
url(r'^logout/sso/$',cas.views.logout, name='logout_sso'),
url(r'^login/$', login, name='login'),
url(r'^logout/$', logout, name='logout'),
]
As you can see, there are also general login
and logout
urls. I have implemented these views as follows:
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
def login(request):
param = request.META['QUERY_STRING']
return render(request, 'login.html', {
'param': param,
})
@login_required
def logout(request):
if hasattr(request.user, 'employee'):
return redirect('logout_sso')
else:
return redirect('logout_regular')
The general login template simply displays the links the to login_sso
URL and the login_regular
URL. The logout
view always returns a redirect to the correct logout URL.
This code works perfectly and as intended, with one major problem: One day, the CAS server might return an employee username that is, by pure coincedence, the same as an already existing regular user. The CAS Response Callback will happily retrieve the existing user and initiate the employee
profile. The end result is that this user now has both a regular_user
attribute and an employee
attribute. Even worse, the account of the original regular user has now been hijacked by the employee who happens to have the same username. I would like to have regular users and employees with the same username coexist peacefully. How can I accomplish this?