我应该如何在Django应用程序中使用AAD实现用户SSO(使用Django Microsoft身份验证后端模块)?
我正在开发一个Django(2.2.3)应用程序,安装了Azure AD来处理SSO。我能够按照快速启动文档的要求,使用我的Microsoft标识或添加到Django用户表中的标准用户名和密码登录到Django管理面板。这一切都是现成的,很好 我的问题(真的)很简单,就是“我下一步做什么?”。从用户的角度来看,我希望他们:我应该如何在Django应用程序中使用AAD实现用户SSO(使用Django Microsoft身份验证后端模块)?,django,azure,azure-active-directory,single-sign-on,Django,Azure,Azure Active Directory,Single Sign On,我正在开发一个Django(2.2.3)应用程序,安装了Azure AD来处理SSO。我能够按照快速启动文档的要求,使用我的Microsoft标识或添加到Django用户表中的标准用户名和密码登录到Django管理面板。这一切都是现成的,很好 我的问题(真的)很简单,就是“我下一步做什么?”。从用户的角度来看,我希望他们: 导航到我的应用程序(example.com/或example.com/content)-Django会意识到它们没有经过身份验证,而且 自动将它们重定向到同一窗口中的SSO
- 自动将它们重定向到同一窗口中的SSO门户,或
- 将他们重定向到example.com/login,这要求他们单击将打开SSO的按钮 窗口中的门户(这是默认管理情况下发生的情况)
@login\u所需的页面(example.com/content)
def index(request):
if request.user.is_authenticated:
return redirect("/content")
else:
return redirect("/login")
我最初的想法是简单地将重定向(“/login”)
更改为重定向(授权url)
——这就是我的问题开始的地方
据我所知,没有任何方法可以获取上下文处理器的当前实例(?)或microsoft\u auth
插件的后端来调用authorization\u url()
函数并从views.py
重定向用户
好的。。。然后我想我应该实例化生成authURL的MicrosoftClient
类。这不起作用-不是100%确定原因,但它认为这可能与后端/上下文处理器上实际的MicrosoftClient
实例使用的某些状态变量与我的实例不一致有关
最后,我尝试模拟自动/admin
页面所做的操作—为用户提供一个SSO按钮供其单击,并在单独的窗口中打开Azure门户。在仔细研究之后,我意识到我基本上也有同样的问题——身份验证URL作为内联JS传递到管理员登录页面模板中,该模板稍后用于在客户端异步创建Azure窗口
作为一项健全性检查,我尝试手动导航到管理员登录页面中显示的auth URL,这确实起了作用(尽管重定向到/content
没有起作用)
在这一点上,考虑到我认为我是在为自己制造困难,我觉得我在以完全错误的方式处理这整件事。遗憾的是,我找不到任何关于如何完成这部分过程的文档
那么,我做错了什么 再过几天,我终于自己解决了这些问题,并进一步了解了Django的工作原理 我缺少的链接是来自(第三方)Django模块的上下文处理器如何/在何处将其上下文传递到最终呈现的页面。我没有意识到,默认情况下,我也可以在任何模板中访问microsoft_auth包中的变量(例如模板中使用的
授权\u url
)。知道了这一点,我能够实现一个稍微简单的版本,这个版本与管理面板使用的基于JS的登录过程相同
假设将来读到这篇文章的人都经历了与我相同的(学习)过程(特别是这个软件包),我可能会猜到下面几个问题
第一个是“我已成功登录…我如何代表用户做任何事情?!”。有人会假设您将获得用户的访问令牌以用于将来的请求,但在编写此包时,默认情况下似乎没有以任何明显的方式完成。软件包的文档只能让您登录到管理面板
(在我看来,不是很明显)答案是您必须将MICROSOFT\u AUTH\u AUTHENTICATE\u HOOK
设置为一个可以在成功身份验证时调用的函数。它将被传递给登录用户(模型)和他们的令牌JSON对象,供您随意使用。经过深思熟虑,我选择使用AbstractUser
扩展我的用户模型,并将每个用户的令牌与其他数据一起保留
models.py
class User(AbstractUser):
access_token = models.CharField(max_length=2048, blank=True, null=True)
id_token = models.CharField(max_length=2048, blank=True, null=True)
token_expires = models.DateTimeField(blank=True, null=True)
aad.py
from datetime import datetime
from django.utils.timezone import make_aware
def store_token(user, token):
user.access_token = token["access_token"]
user.id_token = token["id_token"]
user.token_expires = make_aware(datetime.fromtimestamp(token["expires_at"]))
user.save()
设置.py
MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"
请注意MICROSOFT\u AUTH\u EXTRA\u SCOPES
设置,这可能是您的第二个/次要问题-包中的默认范围设置为SCOPE\u MICROSOFT=[“openid”、“email”、“profile”]
,如何添加更多内容并不明显。我需要添加用户。至少要阅读。请记住,该设置需要一个以空格分隔的作用域字符串,而不是一个列表
一旦您拥有了访问令牌,就可以自由地向Microsoft Graph API发出请求。它们在这方面非常有用。因此我在Django中基于。
希望这能帮助一些人
import logging
import uuid
from os import getenv
import msal
import requests
from django.http import JsonResponse
from django.shortcuts import redirect, render
from rest_framework.generics import ListAPIView
logging.getLogger("msal").setLevel(logging.WARN)
# Application (client) ID of app registration
CLIENT_ID = "<appid of client registered in AD>"
TENANT_ID = "<tenantid of AD>"
CLIENT_SECRET = getenv("CLIENT_SECRET")
AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID
# This resource requires no admin consent
GRAPH_ENDPOINT = 'https://graph.microsoft.com/v1.0/me'
SCOPE = ["User.Read"]
LOGIN_URI = "https://<your_domain>/login"
# This is registered as a redirect URI in app registrations in AD
REDIRECT_URI = "https://<your_domain>/authorize"
class Login(ListAPIView):
'''initial login
'''
def get(self, request):
session = request.session
id_token_claims = get_token_from_cache(session, SCOPE)
if id_token_claims:
access_token = id_token_claims.get("access_token")
if access_token:
graph_response = microsoft_graph_call(access_token)
if graph_response.get("error"):
resp = JsonResponse(graph_response, status=401)
else:
resp = render(request, 'API_AUTH.html', graph_response)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
else:
session["state"] = str(uuid.uuid4())
auth_url = build_auth_url(scopes=SCOPE, state=session["state"])
resp = redirect(auth_url)
return resp
class Authorize(ListAPIView):
'''authorize after login
'''
def get(self, request):
session = request.session
# If states don't match login again
if request.GET.get('state') != session.get("state"):
return redirect(LOGIN_URI)
# Authentication/Authorization failure
if "error" in request.GET:
return JsonResponse({"error":request.GET.get("error")})
if request.GET.get('code'):
cache = load_cache(session)
result = build_msal_app(cache=cache).acquire_token_by_authorization_code(
request.GET['code'],
# Misspelled scope would cause an HTTP 400 error here
scopes=SCOPE,
redirect_uri=REDIRECT_URI
)
if "error" in result:
resp = JsonResponse({"error":request.GET.get("error")})
else:
access_token = result["access_token"]
session["user"] = result.get("id_token_claims")
save_cache(session, cache)
# Get user details using microsoft graph api call
graph_response = microsoft_graph_call(access_token)
resp = render(request, 'API_AUTH.html', graph_response)
else:
resp = JsonResponse({"login":"failed"}, status=401)
return resp
def load_cache(session):
'''loads from msal cache
'''
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def save_cache(session,cache):
'''saves to msal cache
'''
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def build_msal_app(cache=None, authority=None):
'''builds msal cache
'''
return msal.ConfidentialClientApplication(
CLIENT_ID, authority=authority or AUTHORITY,
client_credential=CLIENT_SECRET, token_cache=cache)
def build_auth_url(authority=None, scopes=None, state=None):
'''builds auth url per tenantid
'''
return build_msal_app(authority=authority).get_authorization_request_url(
scopes or [],
state=state or str(uuid.uuid4()),
redirect_uri=REDIRECT_URI)
def get_token_from_cache(session, scope):
'''get accesstoken from cache
'''
# This web app maintains one cache per session
cache = load_cache(session)
cca = build_msal_app(cache=cache)
accounts = cca.get_accounts()
# So all account(s) belong to the current signed-in user
if accounts:
result = cca.acquire_token_silent(scope, account=accounts[0])
save_cache(session, cache)
return result
def microsoft_graph_call(access_token):
'''graph api to microsoft
'''
# Use token to call downstream service
graph_data = requests.get(
url=GRAPH_ENDPOINT,
headers={'Authorization': 'Bearer ' + access_token},
).json()
if "error" not in graph_data:
return {
"Login" : "success",
"UserId" : graph_data.get("id"),
"UserName" : graph_data.get("displayName"),
"AccessToken" : access_token
}
else:
return {"error" : graph_data}
导入日志
导入uuid
从操作系统导入getenv
进口msal
导入请求
从django.http导入JsonResponse
从django.shortcuts导入重定向,渲染
从rest\u framework.generics导入ListAPIView
logging.getLogger(“msal”).setLevel(logging.WARN)
#应用程序注册的应用程序(客户端)ID
客户_ID=“”
租户_ID=“”
CLIENT\u SECRET=getenv(“CLIENT\u SECRET”)
权威=”https://login.microsoftonline.com/“+租户ID
#此资源不需要管理员同意
图https://graph.microsoft.com/v1.0/me'
作用域=[“User.Read”]
登录\u URI=”https:///login"
#这在AD中的应用程序注册中注册为重定向URI
重定向_URI=”https:///authorize"
类登录(ListAPIView):
''初始登录
'''
def get(自我,请求):
会话=r