Azure AD API权限与Expose an API区别及管理员同意问题排查
我目前正在开发一款React Web应用,将使用微软的MSAL包完成用户身份认证,确保仅所属租户内的用户可以访问对应API。
- 已构建名为TARGET_APP的HTTP函数应用,内置用于访问业务数据并返回结果的Python函数,且已将该应用注册至Azure AD企业应用列表。
- 根据官方文档对「On Behalf Of(代理)」调用流程的要求,额外注册了代表React客户端应用的应用,命名为CALLER_APP,已完成该应用注册,并配置了所需的权限范围,包括
email、user.read以及TARGET_APP公开的API权限。
CALLER_APP的权限配置示例如下:
客户端通过MSAL携带图中所示的权限范围发起CALLER_APP授权请求时,收到了「需要管理员同意」的提示。
以下是认证流程代码片段(handleLogin为触发认证的入口函数):
const msalConfig = { auth: { authority: "https://login.microsoftonline.com/MY_TENANT/", clientId: "CALLER_APP_CLIENT_ID", redirectUri, postLogoutRedirectUri: redirectUri }, cache: { cacheLocation: "localStorage" } } // 实际调用的caller范围已用"CALLER_APP_SCOPE"占位替换 const loginRequest = { scopes: ["CALLER_APP_SCOPE", "user.read", "email"] }; async function handleLogin(instance) { const loginUrl = await getLoginUrl(instance, loginRequest); const loginResult = await launchWebAuthFlow(instance, loginUrl); // 获取令牌 const { accessToken } = await acquireToken(instance, loginRequest); console.log(accessToken) } /** * 生成登录url */ async function getLoginUrl(instance, request) { return new Promise((resolve, reject) => { instance.loginRedirect({ ...request, onRedirectNavigate: (url) => { resolve(url); return false; } }).catch(reject); }); } /** * 唤起Web授权流程 */ async function launchWebAuthFlow(instance, url) { return new Promise((resolve, reject) => { chrome.identity.launchWebAuthFlow({ interactive: true, url }, (responseUrl) => { // 响应url包含hash(登录、获取令牌类请求) if (responseUrl.includes("#")) { instance.handleRedirectPromise(`#${responseUrl.split("#")[1]}`) .then(resolve) .catch(reject) } else { // 登出类请求 resolve(); } }) }) } /** * 先尝试静默获取访问令牌,失败则回退到交互模式 */ async function acquireToken(instance, request) { return instance.acquireTokenSilent(request).then((response) => { console.log(response.accessToken); }).catch(async (error) => { console.error(error); storage.set({'loggedState': false}); }); }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
上述大部分代码直接参考官方文档编写。调用handleLogin函数可正常发起认证流程,但使用微软账号登录时,会弹出「应用需要访问你组织中仅管理员可授予的资源权限」的提示。
已反复核对所有配置的权限范围,确认没有设置需要管理员同意的权限;同时进入企业应用的用户同意与权限设置页,开启了低影响权限范围的用户同意功能,配置如下图所示:
企业应用配置:
被归类为低影响的3项权限就是前文提到的三个范围:email、user.read、allow-caller。
测试发现,如果改为进入CALLER_APP的「Expose an API」边栏选项卡,在其中创建权限范围,并在MSAL调用时使用该自建范围,认证流程可以完整走完,能成功获取bearer token,也可以正常调用API满足业务需求,但该实现方式并未在官方文档及查阅的参考资料中提及。
现有两个疑问:
- 为什么当前业务场景下不适合使用「Expose an API」的配置方式?
- 为什么配置API权限时会触发管理员同意的要求?
触发管理员同意要求的核心原因
按排查优先级从高到低,常见原因如下:
- TARGET_APP侧暴露API范围时未开放用户同意权限。进入TARGET_APP的「Expose an API」配置页,找到创建的
allow-caller范围,检查「谁可以同意?」配置项,如果选中的是「仅管理员」,哪怕租户全局开启了低影响权限用户同意,普通用户也无法自行授权,必须管理员同意才能继续,改成「管理员和用户」即可解决。 - 请求权限时使用的范围格式错误。Azure AD自定义API的委托权限范围必须严格遵循
api://<TARGET_APP的客户端ID>/<范围名称>格式,如果漏写了APP ID URI前缀、错将CALLER_APP的客户端ID作为前缀,AAD会将请求识别为无效/未授权权限,直接触发管理员同意提示。 - 添加API权限时误选了权限类型。在CALLER_APP中添加TARGET_APP的权限时,如果选的是「应用权限」而非「委托权限」,所有应用权限默认都要求管理员同意,不存在用户自主同意的选项,只有委托权限支持普通用户授权。
不适合直接在CALLER_APP配置「Expose an API」的原因
直接在CALLER_APP上自建范围拿到的令牌,受众(aud声明)是CALLER_APP本身,而非后端服务TARGET_APP。TARGET_APP做令牌校验时,第一步就会校验aud字段是否匹配自身的应用ID,受众不匹配的令牌会直接被判定为无效。
更关键的是这种方式完全不符合OBO流程的设计要求:OBO流程需要客户端先获取针对TARGET_APP的委托权限令牌,将该令牌传给TARGET_APP后,TARGET_APP才能用它兑换访问下游资源(比如Graph API、其他Azure服务)的令牌。如果传入的是受众为CALLER_APP的令牌,TARGET_APP根本无法完成OBO令牌兑换,后续所有涉及用户身份访问下游服务的逻辑都会报错。
内容的提问来源于stack exchange,提问作者John Smith

