Django Microsoft SSO Integration State Mismatch (Django + Azure App Service)

I’m integrating Microsoft SSO into my Django app, and I’m encountering a "State Mismatch" error during the login process. This error occurs when the state parameter, which is used to prevent cross-site request forgery (CSRF) attacks, doesn’t match the expected value.

What’s Happening: During the login process, my Django app generates a state parameter and stores it in the user’s session. When the user is redirected back to my app from Microsoft after authentication, the state parameter returned by Microsoft should match the one stored in the session. However, in my case, the two values don’t match, resulting in a State Mismatch Error.

ERROR:django.security.SSOLogin: State mismatch during Microsoft SSO login.

State received from the callback URL:

state = request.GET.get('state') State stored in the session during SSO initiation: session_state = request.session.get('oauth2_state')

In the logs, I can see that the session state is either missing or mismatched. Here's the log snippet:

Received state in callback: b8bfae27-xxxx-xxxx-xxxxxxxxx Session state before validation: None The callback state is b8bfae27-xxx-xxxx-xxxx-xxxxxxxxxxx, but the session state is None. This leads to a state mismatch and ultimately the login failure.

Expected state: The state value generated by my app and stored in the session.

Returned state: The state value returned by Microsoft after the user completes authentication.

Session ID: The session key for the user during the login attempt.

Django & Azure Configuration: Django version: 5.0.6

Python version: 3.12

SSO Integration Package: django-microsoft-sso Cache backend: LocMemCache (planning to switch to Redis) Azure App Service: Hosted with App Service Plan for deployment.

Time Zone: Central Time (US & Canada) on local development and UTC on the Azure server.

Session Engine: Using default Django session engine with database-backed sessions (django.contrib.sessions.backends.db).

What I’ve Tried: Session Persistence: I verified that session data is being correctly stored and retrieved during the login process.

Time Synchronization: I checked that the time on my server and Microsoft’s authentication server is synchronized to avoid potential timing issues.

Cache Settings: I’m currently using LocMemCache for caching and session management in local development, but I suspect this may be causing issues in Azure due to its lack of persistence across multiple instances.

SSO Settings: I reviewed the Microsoft SSO settings and ensured the correct URLs and callback mechanisms are being used.


python

from django.shortcuts import render, redirect

from django.urls import reverse

from django.contrib.auth import login

from django.utils.timezone import now

from django.http import JsonResponse

from django.views.decorators.csrf import csrf_exempt

import binascii

import os

import logging

logger = logging.getLogger(__name__)

View to handle login failure
def login_failed(request):

Log basic failure info
logger.debug(f"Login failed at {now()}.")

Render failure message
context = {'message': 'We were unable to log you in using Microsoft SSO.'}

return render(request, 'claims/login_failed.html', context)

View to handle Microsoft SSO callback
u/csrf_exempt

def microsoft_sso_callback(request):

Log basic info for debugging
logger.debug(f"SSO callback triggered at {now()}")

Retrieve state from the callback and session
state = request.GET.get('state')

session_state = request.session.get('oauth2_state')

Check for state mismatch or missing state
if not state or state != session_state:

logger.error(f"State mismatch or state missing. Received: {state}, Expected: {session_state}")

request.session.flush() # Clear session to test if a fresh session resolves it

return redirect(reverse('login_failed'))

Process the Microsoft user data
microsoft_user = getattr(request, 'microsoft_user', None)

if microsoft_user:

email = microsoft_user.get('email')

if email:

try:

user = User.objects.get(email=email)

Log the user in using the correct backend
login(request, user, backend='django.contrib.auth.backends.ModelBackend')

return redirect('admin:index')

except User.DoesNotExist:

return redirect(reverse('login_failed'))

else:

return redirect(reverse('login_failed'))

else:

return redirect(reverse('login_failed'))

View to initiate the Microsoft SSO login process
def sso_login(request):

Generate a secure random state
state = binascii.hexlify(os.urandom(16)).decode()

Store state in session and save
request.session['oauth2_state'] = state

request.session.save()

Build the Microsoft login URL
login_url = 'https://login.microsoftonline.com/{}/oauth2/v2.0/authorize'.format(settings.MICROSOFT_SSO_TENANT_ID)

params = {

'client_id': settings.MICROSOFT_SSO_APPLICATION_ID,

'response_type': 'code',

'redirect_uri': settings.MICROSOFT_SSO_REDIRECT_URI,

'response_mode': 'query',

'scope': ' '.join(settings.MICROSOFT_SSO_SCOPES),

'state': state,

}

login_url_with_params = f"{login_url}?{'&'.join(f'{key}={value}' for key, value in params.items())}"

return redirect(login_url_with_params)
Django Settings:
USE_TZ = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_SECURE = True
SESSION_SAVE_EVERY_REQUEST = True
MICROSOFT_SSO_ENABLED = True
MICROSOFT_SSO_APPLICATION_ID = 'My-App-ID'
MICROSOFT_SSO_CLIENT_SECRET = 'My-Client-Secret'
MICROSOFT_SSO_TENANT_ID = 'My-Tenant-ID'
MICROSOFT_SSO_REDIRECT_URI = 'http://localhost:8000/xxx/xxxx/'
MICROSOFT_SSO_SCOPES = ['openid', 'profile', 'email']

I connected Redis to the Django project in the settings.py file using port 6379 and was able to log in successfully.

django_microsoft_sso/sso/views.py :

from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth import login as django_login1, logout as django_logout1
from django.utils.timezone import now
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
from django.contrib.auth.models import User  # Add this import
import binascii
import os
import logging

logger = logging.getLogger(__name__)
def home(request):
    return render(request, 'sso/home.html')
def login_failed(request):
    logger.debug(f"Login failed at {now()}.")
    context = {'message': 'We were unable to log you in using Microsoft SSO.'}
    return render(request, 'sso/login_failed.html', context)

@csrf_exempt
def microsoft_sso_callback(request):
    logger.debug(f"SSO callback triggered at {now()}")
    state = request.GET.get('state')
    session_state = request.session.get('oauth2_state')

    if not state or state != session_state:
        logger.error(f"State mismatch or state missing. Received: {state}, Expected: {session_state}")
        request.session.flush() 
        return redirect(reverse('login_failed'))
    microsoft_user = getattr(request, 'microsoft_user', None)
    if microsoft_user:
        email = microsoft_user.get('email')
        if email:
            try:
                user = User.objects.get(email=email)
                django_login1(request, user, backend='django.contrib.auth.backends.ModelBackend')
                return redirect('admin:index')
            except User.DoesNotExist:
                return redirect(reverse('login_failed'))
        else:
            return redirect(reverse('login_failed'))
    else:
        return redirect(reverse('login_failed'))
def sso_login(request):
    state = binascii.hexlify(os.urandom(16)).decode()
    request.session['oauth2_state'] = state
    request.session.save()
    login_url = f'https://login.microsoftonline.com/{settings.MICROSOFT_SSO_TENANT_ID}/oauth2/v2.0/authorize' 
    params = {
        'client_id': settings.MICROSOFT_SSO_APPLICATION_ID,
        'response_type': 'code',
        'redirect_uri': settings.MICROSOFT_SSO_REDIRECT_URI,
        'response_mode': 'query',
        'scope': ' '.join(settings.MICROSOFT_SSO_SCOPES),
        'state': state,
    }
    login_url_with_params = f"{login_url}?{'&'.join(f'{key}={value}' for key, value in params.items())}"
    return redirect(login_url_with_params)
def logout(request):
    django_logout1(request)
    return redirect('home')

django_microsoft_sso/sso/urls.py :

from django.urls import path
from . import views
urlpatterns = [
    path('', views.home, name='home'),
    path('login/', views.sso_login, name='sso_login'),
    path('callback/', views.microsoft_sso_callback, name='microsoft_sso_callback'),
    path('login-failed/', views.login_failed, name='login_failed'),
    path('logout/', views.logout, name='logout'), 
]

django_microsoft_sso/django_microsoft_sso/settings.py :

import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = '<secretkey>)'
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sso',
]
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'django_microsoft_sso.urls'
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
WSGI_APPLICATION = 'django_microsoft_sso.wsgi.application'
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_SAVE_EVERY_REQUEST = True
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}
MICROSOFT_SSO_ENABLED = True
MICROSOFT_SSO_APPLICATION_ID = 'ClientID'
MICROSOFT_SSO_CLIENT_SECRET = 'ClientSecret'
MICROSOFT_SSO_TENANT_ID = 'TenantID'
MICROSOFT_SSO_REDIRECT_URI = 'http://localhost:8000/sso/callback/'
MICROSOFT_SSO_SCOPES = ['openid', 'profile', 'email', 'offline_access']

django_microsoft_sso/django_microsoft_sso/urls.py :

from django.contrib import admin
from django.urls import path, include
from sso import views
urlpatterns = [
    path('admin/', admin.site.urls),
    path('sso/', include('sso.urls')),  
    path('', views.home, name='home'),  
]

login_failed.html :

<!DOCTYPE html>
<html>
<head>
    <title>Login successful</title>
</head>
<body>
    <form action="{% url 'logout' %}" method="post">
        {% csrf_token %}
        <button type="submit">Logout</button>
    </form>
</body>
</html>

home.html:

<!DOCTYPE html>
<html>
<head>
    <title>Home</title>
</head>
<body>
    <h1>Welcome to the Home Page</h1>
    <a href="{% url 'sso_login' %}">Login with Microsoft</a>
</body>
</html>

I added the below redirect URI in the Azure app registration as shown below.

http://localhost:8000/sso/callback/

enter image description here

Before going to run the above Django project, you need to install Redis.

Output :

I ran Redis using the below command,

redis-server.exe

enter image description here

The Django project ran successfully, as shown below.

python manage.py runserver localhost:8000

enter image description here

Browser Output :

I successfully logged in and logged out by clicking the Login with Microsoft and Logout buttons, as shown below.

enter image description here

enter image description here

enter image description here

Back to Top