我应该如何在 Django 应用程序中使用 AAD 实现用户 SSO(使用 Django Microsoft 身份验证后端模块)?
How should I be implementing user SSO with AAD in a Django application (using the Django Microsoft Authentication Backend module)?
我正在开发一个安装了 Django Microsoft Auth 的 Django (2.2.3) 应用程序来处理 Azure AD 的 SSO。我已经能够按照快速入门文档来允许我使用我的 Microsoft 身份或我添加到 Django 用户 table 的标准用户名和密码登录 Django 管理面板。这一切都开箱即用,很好。
我的问题(真的)很简单 "What do I do next?"。从用户的角度来看,我希望他们:
- 导航到我的应用程序(example.com/ 或示例。com/content)- Django 将意识到它们未通过身份验证,或者
- 自动将它们重定向到同一 window 中的 SSO 门户,或
- 将他们重定向到 example.com/login,这要求他们单击将打开 SSO 的按钮
window 中的门户(这是默认管理案例中发生的情况)
- 允许他们登录并通过其 Microsoft 帐户使用 MFA
- 成功后将他们重定向到我的
@login_required
页面(示例。com/content)
目前,在我的导航根目录 (example.com/),我有这个:
def index(request):
if request.user.is_authenticated:
return redirect("/content")
else:
return redirect("/login")
我最初的想法是简单地将 redirect("/login")
更改为 redirect(authorization_url)
- 这就是我的问题开始的地方..
据我所知,没有任何方法可以获取上下文处理器的当前实例(?)或 microsoft_auth
插件的后端来调用 authorization_url()
函数和将用户从 views.py
.
重定向
好的...然后我想我只是实例化生成身份验证 URL 的 MicrosoftClient
class。这没有用——不是 100% 确定为什么,但它认为这可能与 backend/context 处理器上实际 MicrosoftClient
实例使用的某些状态变量与我的不一致这一事实有关实例.
最后,我尝试模仿自动 /admin
页面的功能 - 呈现一个 SSO 按钮供用户单击,然后在单独的 window 中打开 Azure 门户。仔细研究后,我意识到我基本上有同样的问题——auth URL 作为内联 JS 传递到管理登录页面模板,稍后用于异步创建 Azure window客户端。
作为完整性检查,我尝试手动导航到管理员登录页面中显示的身份验证 URL,这确实有效(尽管重定向到 /content
没有).
在这一点上,考虑到我认为我为自己做这件事有多么困难,我觉得我正在以完全错误的方式处理这整件事。遗憾的是,我找不到任何关于如何完成这部分过程的文档。
所以,我做错了什么?!
又花了几天时间,我最终自己解决了这些问题,并且对 Django 的工作原理也有了更多了解。
我缺少的 link 是来自(第三方)Django 模块的 how/where 上下文处理器将它们的上下文传递到最终呈现的页面。我没有意识到 microsoft_auth 包中的变量(例如在其模板中使用的 authorisation_url
)默认情况下也可以在我的任何模板中访问。知道了这一点,我能够实现管理面板使用的相同基于 JS 的登录过程的稍微简单的版本。
假设将来阅读本文的任何人都在经历我所经历的相同(学习)过程(特别是这个包),我也许能够猜出你接下来会遇到的几个问题.. .
第一个是“我已成功登录...我如何代表用户做任何事情?!”。人们会假设您将获得用户的访问令牌以用于将来的请求,但在编写此包时,默认情况下似乎并没有以任何明显的方式执行此操作。该软件包的文档仅能帮助您登录管理面板。
(在我看来,不是很明显)答案是您必须将 MICROSOFT_AUTH_AUTHENTICATE_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()
settings.py
MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"
注意 MICROSOFT_AUTH_EXTRA_SCOPES
设置,这可能是您的 second/side 问题 - 包中的默认范围设置为 SCOPE_MICROSOFT = ["openid", "email", "profile"]
,以及如何添加更多范围并不明显。我至少需要添加 User.Read
。请记住,该设置需要一串 space 分隔范围,而不是列表。
获得访问令牌后,您就可以自由地向 Microsoft Graph 发出请求 API。他们的 Graph Explorer 在解决这个问题上非常有用。
所以我在Django中基于https://github.com/Azure-Samples/ms-identity-python-webapp做了这个自定义视图。
希望这会对某人有所帮助。
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}
我正在开发一个安装了 Django Microsoft Auth 的 Django (2.2.3) 应用程序来处理 Azure AD 的 SSO。我已经能够按照快速入门文档来允许我使用我的 Microsoft 身份或我添加到 Django 用户 table 的标准用户名和密码登录 Django 管理面板。这一切都开箱即用,很好。
我的问题(真的)很简单 "What do I do next?"。从用户的角度来看,我希望他们:
- 导航到我的应用程序(example.com/ 或示例。com/content)- Django 将意识到它们未通过身份验证,或者
- 自动将它们重定向到同一 window 中的 SSO 门户,或
- 将他们重定向到 example.com/login,这要求他们单击将打开 SSO 的按钮 window 中的门户(这是默认管理案例中发生的情况)
- 允许他们登录并通过其 Microsoft 帐户使用 MFA
- 成功后将他们重定向到我的
@login_required
页面(示例。com/content)
目前,在我的导航根目录 (example.com/),我有这个:
def index(request):
if request.user.is_authenticated:
return redirect("/content")
else:
return redirect("/login")
我最初的想法是简单地将 redirect("/login")
更改为 redirect(authorization_url)
- 这就是我的问题开始的地方..
据我所知,没有任何方法可以获取上下文处理器的当前实例(?)或 microsoft_auth
插件的后端来调用 authorization_url()
函数和将用户从 views.py
.
好的...然后我想我只是实例化生成身份验证 URL 的 MicrosoftClient
class。这没有用——不是 100% 确定为什么,但它认为这可能与 backend/context 处理器上实际 MicrosoftClient
实例使用的某些状态变量与我的不一致这一事实有关实例.
最后,我尝试模仿自动 /admin
页面的功能 - 呈现一个 SSO 按钮供用户单击,然后在单独的 window 中打开 Azure 门户。仔细研究后,我意识到我基本上有同样的问题——auth URL 作为内联 JS 传递到管理登录页面模板,稍后用于异步创建 Azure window客户端。
作为完整性检查,我尝试手动导航到管理员登录页面中显示的身份验证 URL,这确实有效(尽管重定向到 /content
没有).
在这一点上,考虑到我认为我为自己做这件事有多么困难,我觉得我正在以完全错误的方式处理这整件事。遗憾的是,我找不到任何关于如何完成这部分过程的文档。
所以,我做错了什么?!
又花了几天时间,我最终自己解决了这些问题,并且对 Django 的工作原理也有了更多了解。
我缺少的 link 是来自(第三方)Django 模块的 how/where 上下文处理器将它们的上下文传递到最终呈现的页面。我没有意识到 microsoft_auth 包中的变量(例如在其模板中使用的 authorisation_url
)默认情况下也可以在我的任何模板中访问。知道了这一点,我能够实现管理面板使用的相同基于 JS 的登录过程的稍微简单的版本。
假设将来阅读本文的任何人都在经历我所经历的相同(学习)过程(特别是这个包),我也许能够猜出你接下来会遇到的几个问题.. .
第一个是“我已成功登录...我如何代表用户做任何事情?!”。人们会假设您将获得用户的访问令牌以用于将来的请求,但在编写此包时,默认情况下似乎并没有以任何明显的方式执行此操作。该软件包的文档仅能帮助您登录管理面板。
(在我看来,不是很明显)答案是您必须将 MICROSOFT_AUTH_AUTHENTICATE_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()
settings.py
MICROSOFT_AUTH_EXTRA_SCOPES = "User.Read"
MICROSOFT_AUTH_AUTHENTICATE_HOOK = "django_app.aad.store_token"
注意 MICROSOFT_AUTH_EXTRA_SCOPES
设置,这可能是您的 second/side 问题 - 包中的默认范围设置为 SCOPE_MICROSOFT = ["openid", "email", "profile"]
,以及如何添加更多范围并不明显。我至少需要添加 User.Read
。请记住,该设置需要一串 space 分隔范围,而不是列表。
获得访问令牌后,您就可以自由地向 Microsoft Graph 发出请求 API。他们的 Graph Explorer 在解决这个问题上非常有用。
所以我在Django中基于https://github.com/Azure-Samples/ms-identity-python-webapp做了这个自定义视图。 希望这会对某人有所帮助。
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}