Angular+NodeJS栈如何通过OBO流让后端获取MS Graph访问权限
解决方案
1. 完成Azure AD应用注册前置配置
- 找到你当前使用的Azure AD应用注册,进入「证书和密码」页面,新建一个客户端密钥,保存好密钥值,后续后端配置会用到
- 进入「API权限」页面,添加Microsoft Graph的委托权限
User.Read,点击「授予管理员同意」(企业租户需要租户管理员操作,个人开发者账号自行同意即可) - 确认你之前配置的自定义API scope(对应代码中的
environment.aad_scope_api)状态正常
2. 前端代码无需调整
你当前app.module.ts的配置已经符合要求:已经在授权请求中同时申请了user.read和自定义API scope,msal拦截器也会自动给发往后端的请求携带正确的访问令牌,不需要修改。
3. 后端Node.js实现OBO令牌兑换逻辑
你现在遇到的核心问题是前端传给后端的令牌受众是你的后端API,不是MS Graph,直接用来调用Graph接口必然返回401,需要通过OBO流兑换成Graph可用的令牌。
首先安装依赖:
npm install @azure/msal-node axios
然后修改login.js代码如下:
const { ConfidentialClientApplication } = require('@azure/msal-node'); const axios = require('axios'); // 初始化MSAL机密客户端,用于OBO令牌兑换 const msalClient = new ConfidentialClientApplication({ auth: { clientId: process.env.AAD_CLIENT_ID, // 和前端使用同一个客户端ID clientSecret: process.env.AAD_CLIENT_SECRET, // 之前生成的客户端密钥 authority: `https://login.microsoftonline.com/${process.env.AAD_TENANT_ID}/` } }); // 重写fetchUser方法 async function fetchUser(req) { // 提取前端传入的访问令牌,去掉Bearer前缀 const incomingToken = req.headers.authorization?.split(' ')[1]; if (!incomingToken) throw new Error('缺少授权令牌'); try { // 第一步:通过OBO流兑换MS Graph访问令牌 const oboResult = await msalClient.acquireTokenOnBehalfOf({ oboAssertion: incomingToken, scopes: ['https://graph.microsoft.com/User.Read'], // 要申请的Graph权限 }); // 第二步:用兑换到的令牌调用MS Graph接口 const graphRes = await axios.get('https://graph.microsoft.com/v1.0/me', { headers: { Authorization: `Bearer ${oboResult.accessToken}` } }); // 第三步:处理返回的用户信息 const userObj = { id: graphRes.data.id, displayName: graphRes.data.displayName, mail: graphRes.data.mail || graphRes.data.userPrincipalName // 补充你需要的其他字段 }; return userObj; } catch (err) { console.error('身份验证失败:', err); throw new Error('用户身份验证失败'); } } // 路由部分调整 router.post('/login', (req, res) => { if (req.session.user) { return res.json(req.session.user); } fetchUser(req) .then(result => { req.session.user = result; res.json(result); }).catch(err => { res.status(401).json({ error: err.message }); }) });
注意事项
- 不要硬编码客户端ID、客户端密钥、租户ID这类敏感信息,统一放到环境变量中管理
- 生产环境可以把兑换到的Graph令牌和用户会话绑定,缓存起来避免每次请求都重复兑换,过期后再重新兑换即可
- 你原代码中的
fetchUser写法存在逻辑问题:回调式的request方法没有封装为Promise,async函数不会等待回调执行完成就会返回,换成axios或者把request包装为Promise更稳妥。
内容的提问来源于stack exchange,提问作者Nico Schuck
相关产品推荐
相关产品推荐

