Django Microsoft SSO 集成状态不匹配(Django + Azure 应用服务)

问题描述 投票:0回答:1

我正在将 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']

python-3.x django azure single-sign-on
1个回答
0
投票

我使用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/

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 MicrosoftLogout 按钮成功登录并注销,如下所示。

enter image description here

enter image description here

enter image description here

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.