Warm tip: This article is reproduced from stackoverflow.com, please click
azure azure-active-directory django single-sign-on

How should I be implementing user SSO with AAD in a Django application (using the Django Microsoft A

发布于 2020-04-07 10:18:58

I'm developing a Django (2.2.3) application with Django Microsoft Auth installed to handle SSO with Azure AD. I've been able to follow the quickstart documentation to allow me to log into the Django Admin panel by either using my Microsoft identity, or a standard username and password I've added to the Django user table. This all works out of the box and is fine.

My question put (really) simply is "What do I do next?". From a user's perspective, I'd like them to:

  1. Navigate to my application (example.com/ or example.com/content) - Django will realise they aren't authenticated, and either
    • automatically redirect them to the SSO portal in the same window, or
    • redirect them to example.com/login, which requires them to click a button that will open the SSO portal in a window (which is what happens in the default admin case)
  2. Allow them to sign in and use MFA with their Microsoft Account
  3. Once successful redirect them to my @login_required pages (example.com/content)

Currently, at the root of my navigation (example.com/), I have this:

    def index(request):
        if request.user.is_authenticated:
            return redirect("/content")
        else:
            return redirect("/login")

My original idea was to simply change the redirect("/login") to redirect(authorization_url) - and this is where my problems start..

As far as I can tell, there isn't any way to get the current instance(?) of the context processor or backend of the microsoft_auth plugin to call the authorization_url() function and redirect the user from views.py.

Ok... Then I thought I'd just instantiate the MicrosoftClient class that generates the auth URL. This didn't work - not 100% sure why, but it think it may have something to do with the fact that some state variable used by the actual MicrosoftClient instance on the backend/context processor is inconsistent with my instance.

Finally, I tried to mimic what the automatic /admin page does - present an SSO button for the user to click, and open the Azure portal in a separate window. After digging around a bit, I realise that I fundamentally have the same problem - the auth URL is passed into the admin login page template as inline JS, which is later used to create the Azure window asynchronously on the client side.

As a sanity check, I tried to manually navigate to the auth URL as it is presented in the admin login page, and that did work (though the redirect to /content didn't).

At this point, given how difficult I think I'm making it for myself, I'm feel like I'm going about this whole thing completely the wrong way. Sadly, I can't find any documentation on how to complete this part of the process.

So, what am I doing wrong?!

Questioner
J.B
Viewed
184
J.B 2020-02-03 18:30

A couple more days at this and I eventually worked out the issues myself, and learned a little more about how Django works too.

The link I was missing was how/where context processors from (third party) Django modules pass their context's through to the page that's eventually rendered. I didn't realise that variables from the microsoft_auth package (such as the authorisation_url used in its template) were accessible to me in any of my templates by default as well. Knowing this, I was able to implement a slightly simpler version of the same JS based login process that the admin panel uses.

Assuming that anyone reading this in the future is going through the same (learning) process I have (with this package in particular), I might be able to guess at the next couple of questions you'll have...

The first one was "I've logged in successfully...how do I do anything on behalf of the user?!". One would assume you'd be given the user's access token to use for future requests, but at the time of writing this package didn't seem to do it in any obvious way by default. The docs for the package only get you as far as logging into the admin panel.

The (in my opinion, not so obvious answer) is that you have to set MICROSOFT_AUTH_AUTHENTICATE_HOOK to a function that can be called on a successful authentication. It will be passed the logged in user (model) and their token JSON object for you to do with as you wish. After some deliberation, I opted to extend my user model using AbstractUser and just keep each user's token with their other data.

models.py

class User(AbstractUser):
    access_token = models.CharField(max_length=2048, blank=True, null=True)
    id_token = models.CharField(max_length=2048, blank=True, null=True)
    token_expires = models.DateTimeField(blank=True, null=True)

aad.py

from datetime import datetime
from django.utils.timezone import make_aware

def store_token(user, token):
    user.access_token = token["access_token"]
    user.id_token = token["id_token"]
    user.token_expires = make_aware(datetime.fromtimestamp(token["expires_at"]))
    user.save()

settings.py

MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"

Note the MICROSOFT_AUTH_EXTRA_SCOPES setting, which might be your second/side question - The default scopes set in the package as SCOPE_MICROSOFT = ["openid", "email", "profile"], and how to add more isn't made obvious. I needed to add User.Read at the very least. Keep in mind that the setting expects a string of space separated scopes, not a list.

Once you have the access token, you're free to make requests to the Microsoft Graph API. Their Graph Explorer is extremely useful in helping out with this.