Nest JS Azure AD 身份验证防护不起作用

问题描述 投票:0回答:1

我在 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
}

我缺少任何步骤吗?

azure-active-directory nestjs azure-ad-msal
1个回答
0
投票

出现此问题的原因是具有

User.Read
范围的令牌适用于 Microsoft Graph API,而不是您的应用程序。

由于令牌的受众 (aud) 声明与 API 的 AZURE_CLIENT_ID 不匹配,验证失败,导致“401 未经授权” 错误。

在您的情况下,在设置应用程序 ID URI 后,公开 API 并在应用程序注册中创建新的 自定义范围,如下所示:

enter image description here

您还可以在应用程序注册中创建应用程序角色,如下所示:

enter image description here

现在,将这些应用程序角色分配给企业应用程序下的用户和组,如下所示:

enter image description here

在生成访问令牌之前,请确保在您的应用程序注册中获得同意后授予这些 API 权限

enter image description here

生成访问令牌时,请确保将 scope 值传递为 “api://appID/access_as_user”

您现在可以验证此令牌并根据登录用户的

roles
scp
声明调用自定义API:

enter image description here enter image description here

如果您想调用 Microsoft Graph 来获取用户配置文件,请跳过验证使用 User.Read 范围生成的令牌,并直接调用

/me
端点。

© www.soinside.com 2019 - 2024. All rights reserved.