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/
Прежде чем приступить к запуску вышеуказанного проекта Django, необходимо установить Redis.
Выход :
Я запустил Redis с помощью следующей команды,
redis-server.exe
Проект Django успешно запущен, как показано ниже.
python manage.py runserver localhost:8000
Вывод браузера :
Я успешно вошел в систему и вышел из нее, нажав кнопки Login with Microsoft и Logout, как показано ниже.