How to setup Azure OAuth 2.0 SSO in Django with Microsoft Graph API

Simran Madhok
6 min readOct 3, 2021

--

Photo by Silas Köhler on Unsplash

OAuth 2.0 (OAuth2) is an authorization protocol that allows users to give access to their resources hosted by a service provider, which is controlled through the user of access tokens.

OAuth doesn’t share password data across services instead uses authorization tokens to provide an identity between the consumers and service providers.

OAuth2.0 is also much faster than the deprecated OAuth1.0 no longer needing to be encrypted on the endpoint since they are encrypted in transit.

Pre-requisites

  1. Python 3+ Django App
  2. Microsoft account either personal or work/school account
  3. An Azure Active Directory Tenant (quick steps to set up AD tenant)
  4. In Production, website must be secured with SSL certification to enable SSO authentication in AD

Step 1. Register Web App on AD App Registration

  • Select App Registrations under Manage section and click open ‘New Registration’ button
  • Register your Django web app with following details:
  1. Set Name to DjangoAppSSO
  2. Set Supported account types to Single Tenant (or your app specific Tenant)
  3. Set Redirect URI to http://localhost:8000/callback under drop-down choice Web
Click open ‘Help me choose’ to understand your application’s target audience
  • On the DjangoAppSSO page, copy the value of Application (client) ID and save it for the next step; choose Certificates & secrets option
  • Select the New client secret button. Enter a descriptive name of your client secret; select either month range options/custom for the expiry date
  • Copy the client secret value before leaving the page as it will not be shown again!
  • During Production environment, go to Authentication tab; and change the value of Redirect URI to your deployed web app’s callback URL
Above instructions applied during Production environment only

Microsoft guidelines available here.

Step 2. PIP install packages

With the app registration config in place, we’ll prepare our web application to integrate OAuth SSO as the Authentication protocol.

Ensure to install below libraries compatible to your PY version by prefixing python3 -m pip install PACKAGE_NAME:

pip install msal
pip install PyYAML
pip install python-dateutil

Step 3. Configure OAuth settings

Next we’ll integrate MSAL for Python library within our application by creating the oauth_setttings.yml file in root DIR.

app_id: "YOUR_APP_ID_HERE" 
app_secret: "YOUR_APP_SECRET_HERE"
redirect: "http://localhost:8000/callback"
scopes:
- user.read
- mailboxsettings.read
- calendars.readwrite authority: "https://login.microsoftonline.com/YOUR_APP_CLIENT_ID"

Note to make:

  • Above .yml contains private ID, app secret; hence advisable to not push it to Git repo (add in .gitignore)
  • In Production setup, the site domain/IP address would replace “http://localhost:8000” section of redirect attribute.
  • If your application is multi-tenant, the AUTH URL changes as follows:
"https://login.microsoftonline.com/common"

Step 4. Create URL patterns

# urls.py from django.urls import path 
from . import views
urlpatterns = [ []\
path('', views.home, name='home'),
path('signin', views.sign_in, name='signin'),
path('signout', views.sign_out, name='signout'),
path('callback', views.callback, name='callback'),
]

Step 5. Setup AUTH and GRAPH helper functions

Under your Django app DIR, create an auth_helper.py file with content —

import yaml
import msal
import os
import time

# Load the oauth_settings.yml file located in your app DIR
stream = open('oauth_settings.yml', 'r')
settings = yaml.load(stream, yaml.SafeLoader)

def load_cache(request):
# Check for a token cache in the session
cache = msal.SerializableTokenCache()
if request.session.get('token_cache'):
cache.deserialize(request.session['token_cache'])
return cache

def save_cache(request, cache):
# If cache has changed, persist back to session
if cache.has_state_changed:
request.session['token_cache'] = cache.serialize()

def get_msal_app(cache=None):
# Initialize the MSAL confidential client
auth_app = msal.ConfidentialClientApplication(
settings['app_id'],
authority=settings['authority'],
client_credential=settings['app_secret'],
token_cache=cache)
return auth_app

# Method to generate a sign-in flow
def get_sign_in_flow():
auth_app = get_msal_app()
return auth_app.initiate_auth_code_flow(
settings['scopes'],
redirect_uri=settings['redirect'])

# Method to exchange auth code for access token
def get_token_from_code(request):
cache = load_cache(request)
auth_app = get_msal_app(cache)

# Get the flow saved in session
flow = request.session.pop('auth_flow', {})
result = auth_app.acquire_token_by_auth_code_flow(flow, request.GET)
save_cache(request, cache)

return result


def store_user(request, user):
try:
request.session['user'] = {
'is_authenticated': True,
'name': user['displayName'],
'email': user['mail'] if (user['mail'] != None) else user['userPrincipalName'],
'timeZone': user['mailboxSettings']['timeZone'] if (user['mailboxSettings']['timeZone'] != None) else 'UTC'
}
except Exception as e:
print(e)

def get_token(request):
cache = load_cache(request)
auth_app = get_msal_app(cache)

accounts = auth_app.get_accounts()
if accounts:
result = auth_app.acquire_token_silent(
settings['scopes'],
account=accounts[0])
save_cache(request, cache)

return result['access_token']

def remove_user_and_token(request):
if 'token_cache' in request.session:
del request.session['token_cache']

if 'user' in request.session:
del request.session['user']

The auth_helper functions will assign, authenticate and remove user’s from the application session during sign-in/sign-out process.

The get_sign_in_flow generates an authorization URL, and the get_token_from_code method exchanges the authorization response for an access token.

Next, create graph_helper.py file with content —

import requests
import json

graph_url = 'https://graph.microsoft.com/v1.0'

def get_user(token):
# Send GET to /me
user = requests.get('{0}/me'.format(graph_url),
headers={'Authorization': 'Bearer {0}'.format(token)},
params={
'$select':'displayName,mail,mailboxSettings,userPrincipalName'})
return user.json()

In above snippet, the get_user method makes a GET request to the Microsoft Graph /me endpoint to get the user’s profile, using the access token you acquired previously.

Additionally, you can access calendar/events, mail, groups and files associated with the authenticated user (read more).

Step 5. Enable User session Sign-In Sign-out

from django.shortcuts import render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from tutorial.auth_helper import get_sign_in_flow, get_token_from_code, store_user, remove_user_and_token, get_token
from tutorial_app.graph_helper import *
def home(request):
context = initialize_context(request)
return render(request, 'tutorial_app/home.html', context)
def initialize_context(request):
context = {}
error = request.session.pop('flash_error', None)
if error != None:
context['errors'] = []
context['errors'].append(error)
# Check for user in the session
context['user'] = request.session.get('user'{'is_authenticated': False})
return context
def sign_in(request):
# Get the sign-in flow
flow = get_sign_in_flow()
# Save the expected flow so we can use it in the callback
try:
request.session['auth_flow'] = flow
except Exception as e:
print(e)
# Redirect to the Azure sign-in page
return HttpResponseRedirect(flow['auth_uri'])
def sign_out(request):
# Clear out the user and token
remove_user_and_token(request)
return HttpResponseRedirect(reverse('home'))
def callback(request):
# Make the token request
result = get_token_from_code(request)
#Get the user's profile from graph_helper.py script
user = get_user(result['access_token'])
# Store user from auth_helper.py script
store_user(request, user)
return HttpResponseRedirect(reverse('home'))

In home.html, create a sign-in button with href set as follows:

<a href="{% url 'signin' %}">Sign in</a>

The sign-in action will generate the Azure AD sign-in URL i.e. https://login.microsoftonline.com

It will save the flow generated by the OAuth client and then redirect the browser to the Azure AD sign-in page.

After the sign-in is complete and the user is successfully authenticated, the callback action uses the saved flow and the query string to request an access token.

It then redirects back to home page with the response (it also contains a temporary empty error key-value pair in case sign-in failure occurs).

Step 6. Test User Authentication with O365 SSO Login

Finally, restart the server and run your application with py manage.py runserver; open the browser to —

http://localhost:8000

Ensure to not run the application with http://127.0.0.1:8000, since the MSAL Authenticator only works with HTTPS protocol for production IP and HTTP protocol over /localhost.

Go through the sign-in process, successfully redirecting user to home page.

You may additionally set-up logout button in home.html template, clicking on which the user will be removed from the session, and the session would be reset.

Useful Tips

Once you have enabled your website to allow OAuth SSO login access to all user assignment, you may optionally restrict site access to Users and Groups.

Inside Azure app portal, under Manage, select Users and Groups section > Add user/group.

Optionally assign app role to selected user/groups if predefined.

MSAL Access tokens are short-lived only for a span of an hour post which it would expire. This is where the refresh token can be requested from MSAL’s acquire_token_silent method.

With above process, the user would not be required to pass in credentials every one hour during application usage.

Other articles that helped me along the way

--

--

Simran Madhok

Software Engineer — Python Full Stack Developer — Mobile Application Development