Django and CAS Authentication

This thread carries on from a thread which started on another topic, specifically: URL woes.

In short, Ken and I were discussing ways to implement Central Authentication Service (CAS) in Django in a way which could support multiple CAS servers, i.e one for an Irish university and one for a Spanish university performing authentication for a single Django service.

On to the thread continuation.

Hi Ken,

We’re definitely on the same page about authentication versus authorisation but is entirely possible that I have gotten myself tangled up in a Django plugin soup. Having django-cas-ng map a successful authentication from a CAS server to an instance of a user model in my DB is absolutely what I need, but like you mentioned there appears that there will be a mismatch with the user’s provided email and the username that will come from CAS - in this case it comes from Kerberos.

I may have been looking at this from the wrong angle and trying to solve a problem which may not actually exist. If I move away from the constraint of having a predefined user from uni X, i.e. sally@unix.edu in my database before they login for the first time, I could instead just have django-cas-ng create an instance of a user sally-from-kerberos if she doesn’t already exist. That being said, when I wrote in my last post that I could accept a user’s email, it wasn’t for the purposes of authentication, but rather just to get the CAS service to which I should redirect the user.

The next issue I will have is that I will have multiple universities which use different instances of CAS. Like you said, there is the possibility of using the django-cas-server (more reading needed) but it looks to have a nice way of authenticating users against an external CAS server which can easily be passed an instance of .cas.CASClient which one can pass into .federate.CASFederateValidateUser(). Making multiple views, one for each university should be quite straightforward when compared with my previous thinking around extensively reworking the django-ng-client.

If you were to go down the path where I used the django-ng-client to authenticate against my own instance of django-cas-server, would you split the functions into two seperate Django instances?

Addressing the directed question first - followed by some other thoughts on this:

That’s what we did. They’re both running behind the same nginx instance on the same server. But they’re using separate uwsgi instances and separate venvs.
One of the reasons we went that route is that as of today, the PyPI release of django_cas_server doesn’t work with Django newer than 3.0. (There is an existing PR for Django 3.1 support, but it appears to be having Travis issues preventing it from being merged in. If it were a real issue for us to have that instance on Django 3.1, we’d probably go ahead and merge that PR for our own use.)

Anyway, since that CAS server is intended to be used by what is looking now to be as many as 6 different applications, we didn’t want it to be tied to any one of them.

That’s very easy to do. And, if you can find out what additional attributes are exposed for a particular user, you can build an associated profile object for that user as well. (If such a thing has any value in your app.)

The only aspect of doing this that I would have any concerns about is the case where you have a WhitesellK from UniA and a WhitesellK from UniB and where both CAS providers return only the user name WhitesellK. (That’s one situation where retrieving the additional attributes from the CAS server may be of value.)

By default, this works by presenting a single login page with a select list of CAS providers. It certainly seems like it would be reasonably easy to replace that login page with one that, given the provider (say, from having an email address entered in a previous page), renders a different template and “pre-identifies” which provider to use for the actual authentication step.

Hi Ken,

Thank you, you’ve really helped me navigate the world of Django and CAS.

Last night I hooked up the following things:

  • Setup the CAS Login URL which redirects to the university’s development CAS Login
  • A URL to which the CAS server redirects. This URL then generates a token and redirects to our frontend with url kwargs access_token and refresh_token
  • My frontend developer put some logic in the frontend to authenticate user requests with the returned tokens.

This all seems to be working quite well, albeit it for a single CAS provider. That’s fine for the the time being as we only have one partner university using CAS at this time.

One of the issues I face is that django-cas-ng creates a user in my DB. That’s fine, but together with allauth I end up with an empty string "" as email address in both my user model and the allauth EmailAddress` model.

I see that the CAS server is returning a lot of useful CAS attributes. I’m interested in only a few of them, namely:

      <cas:mail>my-user@mail.edu</cas:mail>
      <cas:displayName>Conor Cunningham</cas:displayName>
      <cas:uid>my-user</cas:uid>
      <cas:givenName>Conor</cas:givenName>
      <cas:sn>Cunningham</cas:sn>

I’ve been going through the django-cas-ng source to try and find out if I can access these attributes anywhere, but I’m not having much joy. I have found where they are parsed in the CASClientV3 class, but I’m not sure how I can get to it in order to access it. I see there is a method which I can easily override in the LoginView called def successful_login() which runs after a successful login. Using the debugger I inspected the contents of the request object and self attributes but didn’t find the XML data which was returned from the CAS server.

Reading the docs has led me to a setting CAS_APPLY_ATTRIBUTES_TO_USER, but it looks as if it will attempt to add all attributes to the user model and I can’t find a setting to define only the ones that I wish to have added. There is also a setting which looks very useful, CAS_RENAME_ATTRIBUTES, but it isn’t much help we still end up with all of the returned attributes being passed to an instance of my User object.

So considering the above, it looks like I will have to write my own CAS Backend which will inherit from the CASBackend.

Well, that was a long winded way of getting to a question or two:

  1. Do you know how I can grab custom XML attributes from the CAS server and use them to create/update my user object?
  2. Have you and your team written your own CAS Backend to achieve something along the lines of what we are discussing?

As always Ken, thank you for your engagement and your time.

Cheers,

Conor

(much snipped for brevity)

I believe you might be looking for the cas_user_authenticated signal. A handler for that signal receives the attributes dict as a named parameter. See CASBackend.authenticate. (It works for us, and we haven’t needed to write a custom backend.)

Hope this helps.
Ken

Fantastic Ken, thank you!

I played around with a custom backend and achieved my goal, but this looks like a better solution with less fuss and less chance of doing something daft.

Cheers,

Conor

1 Like

The signal worked a treat, thank you, Ken!

1 Like