我在 Nestjs 中创建身份验证防护来保护我的端点时遇到了一个问题,
这是我的 authguard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as jwt from 'jsonwebtoken';
import axios from 'axios';
@Injectable()
export class AuthGuard implements CanActivate {
private publicKeys: Record<string, string> = {};
constructor(private readonly configService: ConfigService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException(
'Authorization header missing or invalid',
);
}
const token = authHeader.split(' ')[1];
// Validate the token
try {
const decodedToken = await this.validateToken(token);
// Attach user details to the request
request.user = decodedToken;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid or expired token');
}
}
private async validateToken(token: string): Promise<any> {
const tenantId = this.configService.get<string>('AZURE_TENANT_ID');
const clientId = this.configService.get<string>('AZURE_CLIENT_ID');
const issuer = `https://login.microsoftonline.com/${tenantId}/v2.0`;
if (!tenantId || !clientId) {
throw new Error('AZURE_TENANT_ID or AZURE_CLIENT_ID is not defined');
}
// Decode the token without verifying to extract header
const decoded = jwt.decode(token, { complete: true }) as {
header: { kid: string };
};
if (!decoded || !decoded.header || !decoded.header.kid) {
throw new UnauthorizedException('Invalid token structure');
}
const kid = decoded.header.kid;
// Fetch public keys if not already cached
if (!this.publicKeys[kid]) {
const jwks = await this.fetchJwks(issuer);
this.publicKeys = jwks;
}
const publicKey = this.publicKeys[kid];
if (!publicKey) {
throw new UnauthorizedException('Public key not found');
}
// Verify the token
return jwt.verify(token, publicKey, {
issuer,
audience: clientId,
});
}
private async fetchJwks(issuer: string): Promise<Record<string, string>> {
const response = await axios.get(`${issuer}/discovery/v2.0/keys`);
const jwks = response.data.keys;
const publicKeys: Record<string, string> = {};
for (const key of jwks) {
const keyPem = this.convertJwkToPem(key);
publicKeys[key.kid] = keyPem;
}
return publicKeys;
}
private convertJwkToPem(jwk: any): string {
const modulus = Buffer.from(jwk.n, 'base64').toString('base64');
const exponent = Buffer.from(jwk.e, 'base64').toString('base64');
return `-----BEGIN RSA PUBLIC KEY-----
${modulus}
${exponent}
-----END RSA PUBLIC KEY-----`;
}
}
这是我的控制器 user.controller.ts 来获取用户配置文件:
import {
Controller,
Get,
InternalServerErrorException,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from 'src/auth/auth.guard';
import { Request, Response } from 'express';
import { HttpStatus } from '@nestjs/common';
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
@Get()
@UseGuards(AuthGuard)
async getUserProfile(
@Req() req: Request,
@Res() res: Response,
): Promise<any> {
try {
const response = await this.userService.getUserProfile(
req.session.token!,
);
res.status(HttpStatus.OK).send(response.data);
} catch (error) {
const errMessage = 'Error getting user profile: ' + error.message;
throw new InternalServerErrorException(errMessage);
}
}
}
当我调用 API 进行登录过程时,它成功返回我一个访问令牌,但是当我使用该访问令牌作为授权标头时,它总是显示 401 未经授权:用户未登录,重定向到登录。
这是登录后的回复:
Token Response: {
authority: 'https://login.microsoftonline.com/<some string>/',
uniqueId: '<some string>',
tenantId: '<some string>',
scopes: [ 'openid', 'profile', 'User.Read', 'email' ],
account: {
homeAccountId: '<some string>',
environment: 'login.windows.net',
tenantId: '<some string>',
username: '<some string>',
localAccountId: '<some string>',
name: '<some string>',
nativeAccountId: undefined,
authorityType: 'MSSTS',
tenantProfiles: Map(1) { '<some string>' => [Object] },
idTokenClaims: {
aud: '<some string>',
iss: 'https://login.microsoftonline.com/<some string>/v2.0',
iat: 1731659325,
nbf: 1731659325,
exp: 1731663225,
auth_time: 1731656361,
email: '<some string>',
family_name: '<some string>',
given_name: '<some string>',
groups: [Array],
name: '<some string>',
oid: '<some string>',
preferred_username: '<some string>',
rh: '<some string>',
roles: [Array],
sid: '<some string>',
sub: '<some string>',
tid: '<some string>',
uti: '<some string>',
ver: '2.0',
wids: [Array]
},
idToken: '<some string>'
},
idToken: '<some string>',
idTokenClaims: {
aud: '<some string>',
iss: 'https://login.microsoftonline.com/<some string>/v2.0',
iat: 1731659325,
nbf: 1731659325,
exp: 1731663225,
auth_time: 1731656361,
email: '<some string>',
family_name: '<some string>',
given_name: '<some string>',
groups: [
'<some string>',],
name: '<some string>',
oid: '<some string>',
preferred_username: '<some string>',
rh: '<some string>',
roles: [ 'data.read' ],
sid: '<some string>',
sub: '<some string>',
tid: '<some string>',
uti: '<some string>',
ver: '2.0',
wids: [
'<some string>'
]
},
accessToken: '<some string>',
fromCache: false,
expiresOn: 2024-11-15T09:56:24.000Z,
extExpiresOn: 2024-11-15T11:19:03.000Z,
refreshOn: undefined,
correlationId: '<some string>',
requestId: '<some string>',
familyId: '',
tokenType: 'Bearer',
state: '',
cloudGraphHostName: '',
msGraphHost: '',
code: undefined,
fromNativeBroker: false
}
我缺少任何步骤吗?
出现此问题的原因是具有
User.Read
范围的令牌适用于 Microsoft Graph API,而不是您的应用程序。
由于令牌的受众 (aud) 声明与 API 的 AZURE_CLIENT_ID 不匹配,验证失败,导致“401 未经授权” 错误。
在您的情况下,在设置应用程序 ID URI 后,公开 API 并在应用程序注册中创建新的 自定义范围,如下所示:
您还可以在应用程序注册中创建应用程序角色,如下所示:
现在,将这些应用程序角色分配给企业应用程序下的用户和组,如下所示:
在生成访问令牌之前,请确保在您的应用程序注册中获得同意后授予这些 API 权限:
生成访问令牌时,请确保将 scope 值传递为 “api://appID/access_as_user”。
您现在可以验证此令牌并根据登录用户的
roles
和scp
声明调用自定义API:
如果您想调用 Microsoft Graph 来获取用户配置文件,请跳过验证使用 User.Read 范围生成的令牌,并直接调用
/me
端点。