我正在将 Microsoft SSO 集成到我的 Django 应用程序中,但在登录过程中遇到“状态不匹配”错误。当用于防止跨站请求伪造 (CSRF) 攻击的状态参数与预期值不匹配时,会出现此错误。
发生了什么: 在登录过程中,我的 Django 应用程序会生成一个状态参数并将其存储在用户的会话中。当用户在身份验证后从 Microsoft 重定向回我的应用程序时,Microsoft 返回的状态参数应与会话中存储的状态参数匹配。但是,就我而言,这两个值不匹配,导致状态不匹配错误。
错误:django.security.SSOLogin:Microsoft SSO 登录期间状态不匹配。
从回调 URL 收到的状态:
state = request.GET.get('state')
SSO 启动期间存储在会话中的状态:
session_state = request.session.get('oauth2_state')
在日志中,我可以看到会话状态丢失或不匹配。这是日志片段:
回调中收到的状态:b8bfae27-xxxx-xxxx-xxxxxxxxx 验证前的会话状态:无
回调状态为b8bfae27-xxx-xxxx-xxxx-xxxxxxxxxx,但会话状态为None。这会导致状态不匹配并最终导致登录失败。
预期状态:我的应用程序生成并存储在会话中的状态值。
返回状态:用户完成认证后微软返回的状态值。
会话 ID:用户尝试登录期间的会话密钥。
环境详情:
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).
```text
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.
代码:
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 设置:
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']
我使用6379端口将Redis连接到
settings.py
文件中的Django项目,并且能够成功登录。
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>
我在 Azure 应用程序注册中添加了以下重定向 URI,如下所示。
http://localhost:8000/sso/callback/
在运行上面的Django项目之前,需要安装Redis。
输出:
我使用以下命令运行 Redis,
redis-server.exe
Django项目运行成功,如下图
python manage.py runserver localhost:8000
浏览器输出:
我通过单击 Login with Microsoft 和 Logout 按钮成功登录并注销,如下所示。