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

Я интегрирую Microsoft SSO в свое приложение Django и сталкиваюсь с ошибкой "State Mismatch" во время процесса входа в систему. Эта ошибка возникает, когда параметр state, который используется для предотвращения атак с подделкой межсайтовых запросов (CSRF), не соответствует ожидаемому значению.

Что происходит: В процессе входа в систему мое приложение Django генерирует параметр состояния и сохраняет его в сессии пользователя. Когда пользователь перенаправляется обратно в мое приложение от Microsoft после аутентификации, параметр состояния, возвращаемый Microsoft, должен совпадать с тем, что хранится в сессии. Однако в моем случае эти два значения не совпадают, что приводит к ошибке State Mismatch Error.

ERROR:django.security.SSOLogin: Несоответствие состояния при входе в систему Microsoft SSO.

Состояние, полученное от URL обратного вызова:

state = request.GET.get('state') Состояние, хранящееся в сессии во время инициации SSO: session_state = request.session.get('oauth2_state')

В журналах я вижу, что состояние сессии либо отсутствует, либо не совпадает. Вот фрагмент журнала:

Полученное состояние в обратном вызове: b8bfae27-xxxx-xxxx-xxxxxxx Состояние сессии перед проверкой: Нет Состояние обратного вызова - b8bfae27-xxx-xxxx-xxxxxxxxxxx, но состояние сессии - None. Это приводит к несоответствию состояний и, в конечном счете, к неудаче при входе в систему.

Ожидаемое состояние: Значение состояния, сгенерированное моим приложением и сохраненное в сессии.

Возвращенное состояние: Значение состояния, возвращаемое Microsoft после завершения аутентификации пользователя.

Session ID: ключ сессии для пользователя во время попытки входа.

Django & Azure Configuration: Версия Django: 5.0.6

Версия Python: 3.12

SSO Integration Package: django-microsoft-sso Кэш-бэкенд: LocMemCache (планируется переход на Redis) Azure App Service: Хостинг с планом App Service Plan для развертывания.

Часовой пояс: Центральное время (США и Канада) на локальной разработке и UTC на сервере Azure.

Движок сессий: Использование стандартного движка сессий Django с сессиями, поддерживаемыми базой данных (django.contrib.sessions.backends.db).

Что я пробовал: Постоянство сеанса: Я проверил, что данные сеанса правильно сохраняются и извлекаются в процессе входа в систему.

Синхронизация времени: Я проверил, что время на моем сервере и сервере аутентификации Microsoft синхронизировано, чтобы избежать потенциальных проблем со временем.

Настройки кэша: В настоящее время я использую LocMemCache для кэширования и управления сессиями в локальной разработке, но я подозреваю, что это может вызвать проблемы в Azure из-за отсутствия сохранения данных в нескольких экземплярах.

Настройки SSO: Я проверил настройки Microsoft SSO и убедился, что используются правильные URL-адреса и механизмы обратного вызова.


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']

Я подключил Redis к проекту Django в файле settings.py, используя порт 6379, и смог успешно войти в систему.

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>

Я добавил указанный ниже URI перенаправления в регистрацию приложения Azure, как показано ниже.

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

enter image description here

Прежде чем приступить к запуску вышеуказанного проекта Django, необходимо установить Redis.

Выход :

Я запустил Redis с помощью следующей команды,

redis-server.exe

enter image description here

Проект Django успешно запущен, как показано ниже.

python manage.py runserver localhost:8000

enter image description here

Вывод браузера :

Я успешно вошел в систему и вышел из нее, нажав кнопки Login with Microsoft и Logout, как показано ниже.

enter image description here

enter image description here

enter image description here

Вернуться на верх