NestJS中Azure AD公共客户端PKCE配置异常求助
在NestJS中配置Azure AD认证授权时,使用带PKCE配置的Public Client Application,却遇到以下问题:
- 初始配置(平台为Web)时,用户完成认证后返回错误:
invalid_client: Error(s): 7000218 - Timestamp: 2024-10-21 02:49:19Z - Description: AADSTS7000218: 请求正文必须包含以下参数:'client_assertion' 或 'client_secret'。
Trace ID: e76ee3c8-d591-43f5-b939-1a5188441000
Correlation ID: 3711a68c-a1c7-4ddf-a524-0fbd0d68feb2
Timestamp: 2024-10-21 02:49:19Z
- 将平台配置改为SPA后,错误变为:
AADSTS9002325: 跨域授权码兑换需要使用PKCE证明密钥。
相关代码如下:
PublicAuthService.ts
import { Injectable, Logger } from '@nestjs/common'; import { PublicClientApplication, AuthenticationResult } from '@azure/msal-node'; import * as crypto from 'crypto'; @Injectable() export class PublicAuthService { public pca: PublicClientApplication; private readonly logger = new Logger(PublicAuthService.name); private codeVerifier: string; // 临时存储code verifier constructor() { this.pca = new PublicClientApplication({ auth: { clientId: process.env.CLIENT_ID, authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`, }, }); } // 生成code verifier的辅助函数 private generateCodeVerifier(): string { return crypto.randomBytes(32).toString('base64url'); } // 生成code challenge的辅助函数 private generateCodeChallenge(verifier: string): string { return crypto.createHash('sha256').update(verifier).digest('base64url'); } async getAuthUrl(): Promise<string> { this.codeVerifier = this.generateCodeVerifier(); // 存储code verifier const codeChallenge = this.generateCodeChallenge(this.codeVerifier); return this.pca.getAuthCodeUrl({ scopes: ['User.Read'], redirectUri: process.env.REDIRECT_URI, codeChallenge: codeChallenge, codeChallengeMethod: 'S256', }); } async acquireTokenByCode(code: string): Promise<AuthenticationResult> { return this.pca.acquireTokenByCode({ code, scopes: ['User.Read'], redirectUri: process.env.REDIRECT_URI, codeVerifier: this.codeVerifier, // 使用存储的code verifier }); } }
PublicAuthController.ts
import { Controller, Get, Query, Res, HttpStatus } from '@nestjs/common'; import { PublicAuthService } from './public_auth.service'; @Controller('public-auth') export class PublicAuthController { constructor(private readonly publicAuthService: PublicAuthService) {} @Get('login') async login(@Res() res) { const authUrl = await this.publicAuthService.pca.getAuthCodeUrl({ scopes: ['User.Read'], redirectUri: process.env.REDIRECT_URI, }); res.redirect(authUrl); } @Get('redirect') async handleRedirect(@Query('code') code: string, @Res() res) { try { const result = await this.publicAuthService.acquireTokenByCode(code); res.status(HttpStatus.OK).json({ accessToken: result.accessToken, user: result.account, }); } catch (error) { res.status(HttpStatus.UNAUTHORIZED).json({ message: error.message }); } } }
1. 修复登录接口的PKCE参数缺失
你的PublicAuthController中的login方法直接调用了pca.getAuthCodeUrl,但没有传入PKCE相关的codeChallenge和codeChallengeMethod参数,也没有调用PublicAuthService中已经实现的getAuthUrl方法——这会导致授权请求没有携带PKCE信息,Azure AD会将你的应用视为机密客户端,从而要求提供client_secret或client_assertion。
修改login方法如下:
@Get('login') async login(@Res() res) { const authUrl = await this.publicAuthService.getAuthUrl(); res.redirect(authUrl); }
2. 配置正确的Azure AD应用平台类型
- 如果你的NestJS服务是作为后端公开客户端(比如服务端发起认证请求),需要在Azure AD应用的"身份验证"设置中,添加移动和桌面应用平台,而不是Web或SPA。
- SPA平台是为纯前端JavaScript应用设计的,会强制要求跨域场景下的PKCE验证,而后端客户端属于"移动和桌面"类别,更适配你的使用场景。
3. 验证PKCE流程的完整性
确保codeVerifier在授权请求生成时被正确存储,并且在调用acquireTokenByCode时准确传递。注意:codeVerifier必须和生成codeChallenge时使用的完全一致,否则会导致PKCE验证失败。
内容的提问来源于stack exchange,提问作者galih

