使用MSAL+NestJS调用Microsoft Graph API /me/todo/lists时遇401未授权错误
NestJS调用Microsoft Graph API获取待办列表报401 UnknownError问题
配置概述
- 创建
MsGraphService,通过@azure/msal-node初始化ConfidentialClientApplication,配置包含clientId、clientSecret和authority; - 使用
@microsoft/microsoft-graph-client生成Graph客户端,认证提供者静默获取令牌并开启forceRefresh: true确保每次获取新令牌; - 通过
TodoService调用Microsoft Graph API的/me/todo/lists端点,在GraphQL的TodoResolver中调用getTodos方法。
核心代码
MsGraphService
@Injectable() export class MsGraphService { private readonly msalConfig: Configuration; private readonly msalClient: ConfidentialClientApplication; public readonly scopes: string[] = [ 'Tasks.Read', 'Tasks.ReadWrite', 'User.Read', 'User.ReadWrite', 'email', 'openid', 'profile', ]; public readonly redirectUri: string; constructor(private readonly configService: ConfigService) { this.msalConfig = { auth: { clientId: configService.getOrThrow('OAUTH_CLIENT_ID') || '', clientSecret: configService.getOrThrow('OAUTH_CLIENT_SECRET'), authority: `https://login.microsoftonline.com/${this.configService.getOrThrow( 'OAUTH_TENANT_ID', )}`, }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { Logger.debug(message, 'MsGraphService'); }, piiLoggingEnabled: false, logLevel: 3, }, }, }; this.msalClient = new ConfidentialClientApplication(this.msalConfig); this.redirectUri = this.configService.get( 'OAUTH_REDIRECT_URI', 'http://localhost:3000/ms-graph/callback', ); } public GraphClient(userId: string): Client { const client = Client.init({ authProvider: async (done) => { try { const account = await this.msalClient .getTokenCache() .getAccountByHomeId(userId); if (!account) { done('account not defined', null); return; } const response = await this.msalClient.acquireTokenSilent({ scopes: this.scopes, account, forceRefresh: true }); done(null, response.accessToken); Logger.debug(response, 'MsGraphService'); } catch (err) { done(err, null); } }, }); return client; } }
TodoService的getTodos方法
getTodos(userId: string) { try { return this.msService.GraphClient(userId).api(`/me/todo/lists`); } catch (err) { Logger.error(err); } }
错误信息
调用时收到Graph API返回的401未授权错误:
{ statusCode: 401, code: 'UnknownError', requestId: '97df2091-d1c6-43c9-b576-0c32ac123950', date: 2023-08-18T06:28:28.000Z, body: '{"code":"UnknownError","message":"","innerError":{"date":"2023-08-18T08:28:28","request-id":"97df2091-d1c6-43c9-b576-0c32ac123950","client-request-id":"a3a4a284-4398-73df-df1d-3e3f47e02268"}}' }
已确认请求范围包含必要权限,Azure应用注册及权限配置正确,但仍无法定位该UnknownError导致的401错误原因。
排查思路与解决建议
1. 验证Access Token有效性
解析acquireTokenSilent返回的accessToken,重点检查:
aud(受众)是否为https://graph.microsoft.com,确保令牌针对Graph API生成;scp字段是否包含Tasks.Read或Tasks.ReadWrite,确认权限已正确纳入;exp(过期时间)是否在当前时间之后,排除令牌过期问题;oid(用户对象ID)是否与传入的userId匹配,确保账户对应正确。
2. 确认账户匹配
打印getAccountByHomeId(userId)返回的account完整信息,检查homeAccountId、username等字段是否属于目标用户。若缓存中无正确账户,会导致令牌无效。
3. 修复API调用逻辑
当前getTodos仅返回请求构建器,未实际发送请求,需添加.get()方法触发API调用:
getTodos(userId: string) { try { return this.msService.GraphClient(userId).api(`/me/todo/lists`).get(); } catch (err) { Logger.error(err); } }
4. 清理MSAL缓存
即使开启forceRefresh: true,仍可尝试清空令牌缓存,重新获取账户和令牌:
await this.msalClient.getTokenCache().removeAllAccounts();
排查缓存中是否存在过期或无效的账户信息。
5. 确认权限授予状态
在Azure门户应用注册的“API权限”选项卡,检查权限是否已被管理员授予(租户级权限),或用户登录时已同意委托权限。
6. 增加调试日志
- 在
authProvider中打印account对象、acquireTokenSilent完整响应,包括idToken、scopes等细节; - 开启Graph客户端调试日志,查看请求头是否正确携带Bearer令牌:
const client = Client.init({ authProvider: ..., debugLogging: true });
7. 检查端点与租户配置
- 确认
authority中的租户ID正确,多租户应用需设置为https://login.microsoftonline.com/common或对应租户ID; - 尝试调用
/me端点测试基础用户信息获取,排查是否为待办列表端点的特定问题。
内容的提问来源于stack exchange,提问作者Aien Saidi
相关产品推荐
相关产品推荐

