我正在致力于将 Azure AD 身份验证集成到 NestJS 应用程序中。我需要获取委派权限和应用程序权限才能访问 Microsoft Graph 中的资源。具体来说,我需要 User.ReadWrite.All、Application.ReadWrite.All 和 Group.ReadWrite.All 的权限。虽然委派的权限在授权代码流中运行良好,但我正在努力使用客户端凭据流获取应用程序权限。如何在我的 NestJS 应用程序中正确配置和实现客户端凭据流程以获得必要的应用程序权限?
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ConfidentialClientApplication,
CryptoProvider,
ResponseMode,
} from '@azure/msal-node';
import fetch from 'node-fetch';
import { ERRORS } from '../shared/errors';
import { GRAPH_API_URL } from '../shared/constants';
import { UserApi } from '../types/api';
@Injectable()
export class MicrosoftAuthService {
private msalInstance: ConfidentialClientApplication;
private cryptoProvider: CryptoProvider;
constructor(
private readonly configService: ConfigService,
private readonly logger: Logger,
) {
this.msalInstance = new ConfidentialClientApplication({
auth: {
clientId: this.configService.get<string>('AZURE_CLIENT_ID'),
authority: this.configService.get<string>('AZURE_AUTHORITY'),
clientSecret: this.configService.get<string>('AZURE_CLIENT_SECRET'),
},
});
this.cryptoProvider = new CryptoProvider();
}
// Get Auth Code URL for Delegated Permissions
async getAuthCodeUrl(state: string, requestId: string) {
try {
const { challenge, verifier } = await this.cryptoProvider.generatePkceCodes();
const authCodeUrlRequest = {
state,
scopes: [
'User.ReadWrite.All',
'Application.ReadWrite.All',
'Group.ReadWrite.All',
'openid',
],
redirectUri: this.configService.get<string>('AZURE_REDIRECT_URI'),
responseMode: ResponseMode.FORM_POST,
codeChallenge: challenge,
codeChallengeMethod: 'S256',
};
const authCodeUrl = await this.msalInstance.getAuthCodeUrl({
...authCodeUrlRequest,
prompt: 'consent',
extraQueryParameters: {
requestId: requestId,
},
});
return {
authCodeUrl,
pkceCodes: { challenge, verifier },
requestId,
};
} catch (error) {
this.logger.error('Error:', error);
throw new BadRequestException(ERRORS.BAD_REQUEST);
}
}
// Acquire Token By Authorization Code for Delegated Permissions
async acquireTokenByCode(code: string, state: string, session: any) {
try {
const tokenResponse = await this.msalInstance.acquireTokenByCode({
redirectUri: this.configService.get<string>('AZURE_REDIRECT_URI'),
scopes: [
'User.ReadWrite.All',
'Application.ReadWrite.All',
'Group.ReadWrite.All',
'openid',
],
code,
codeVerifier: session.pkceCodes.verifier,
state: state,
});
return tokenResponse.accessToken;
} catch (error) {
this.logger.error('Error acquiring token by code:', error);
throw new BadRequestException(ERRORS.BAD_REQUEST);
}
}
// Acquire Token Using Client Credentials for Application Permissions
async getApplicationAccessToken(): Promise<string> {
try {
const tokenResponse = await this.msalInstance.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
return tokenResponse.accessToken;
} catch (error) {
this.logger.error('Error acquiring application token:', error);
throw new BadRequestException(ERRORS.BAD_REQUEST);
}
}
async getUserInfo(accessToken: string): Promise<UserApi> {
try {
const response = await fetch(`${GRAPH_API_URL}/me`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
const userInfo = (await response.json()) as UserApi;
if (userInfo?.error) {
throw new Error(ERRORS.BAD_REQUEST);
}
return userInfo;
} catch (error) {
this.logger.error('Error fetching user info:', error);
throw new BadRequestException(ERRORS.BAD_REQUEST);
}
}
async getOrganizationInfo(accessToken: string): Promise<any> {
try {
const response = await fetch(`${GRAPH_API_URL}/organization`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.json();
} catch (error) {
this.logger.error('Error fetching organization info:', error);
throw new BadRequestException(ERRORS.BAD_REQUEST);
}
}
}
最初,我注册了一个应用程序并授予了
Application
类型的权限,如下所示:
请注意,您无法使用使用生成的令牌来调用 "/me" 端点 客户端凭证流。或者,使用
而不是 "/me" 在仅应用场景中获取用户信息。/users/userId
要使用客户端凭据流获取令牌并获取 Nest JS 应用程序中的用户信息,我使用了以下示例代码文件:
微软-auth.service.ts:
// src/microsoft-auth/microsoft-auth.service.ts
import { ConfidentialClientApplication, Configuration } from '@azure/msal-node';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MicrosoftAuthService {
private confidentialClient: ConfidentialClientApplication;
private readonly logger = new Logger(MicrosoftAuthService.name);
constructor(private readonly configService: ConfigService) {
const config: Configuration = {
auth: {
clientId: this.configService.get<string>('AZURE_CLIENT_ID'),
authority: this.configService.get<string>('AZURE_AUTHORITY'),
clientSecret: this.configService.get<string>('AZURE_CLIENT_SECRET'),
},
};
this.confidentialClient = new ConfidentialClientApplication(config);
}
async acquireToken(): Promise<string> {
try {
const result =
await this.confidentialClient.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
return result.accessToken;
} catch (error) {
this.logger.error('Failed to acquire token', error);
throw error;
}
}
}
测试.controller.ts:
// src/test/test.controller.ts
import { Controller, Get } from '@nestjs/common';
import { MicrosoftAuthService } from '../microsoft-auth/microsoft-auth.service';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
@Controller('test')
export class TestController {
constructor(
private readonly microsoftAuthService: MicrosoftAuthService,
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {}
@Get('token')
async getToken(): Promise<string> {
const accessToken = await this.microsoftAuthService.acquireToken();
return `Access token: ${accessToken}`;
}
@Get('user')
async getUser(): Promise<any> {
const accessToken = await this.microsoftAuthService.acquireToken();
const userId = this.configService.get<string>('USER_ID');
try {
const response = await firstValueFrom(
this.httpService.get(
`https://graph.microsoft.com/v1.0/users/${userId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
),
);
return response.data;
} catch (error) {
return `Error fetching user info: ${error.message}`;
}
}
}
输出:
当我在浏览器中运行以下 URL 时,我成功获得了访问令牌和 API 响应,如下所示:
http://localhost:3000/test/token
http://localhost:3000/test/user