⚠️

NestJSでpassportライブラリのAuthGuardを継承したGuardで、401エラーのデバッグをする方法

2023/03/15に公開

サンプルコード

JwtCognitoStrategy.ts
import {
  Inject,
  Injectable,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { AppConfigService } from '@server/config/config.service';

import { JwtProviderClaim } from './types';

@Injectable()
export class JwtCognitoStrategy extends PassportStrategy(
  Strategy,
  'jwtCognito',
) {
  constructor(
    @Inject('AppConfigService')
    private configService: AppConfigService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      algorithms: ['RS256'],
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://cognito-idp.${configService.get(
          'aws.region',
        )}.amazonaws.com/${configService.get(
          'cognito.userPoolId',
        )}/.well-known/jwks.json`,
      }),
      passReqToCallback: true,
    });
  }

  public async validate(
    req: Request,
    payload: JwtProviderClaim,
  ): Promise<string> {
    req.body = { sub: payload.sub };
    return payload.sub;
  }
}
AuthGuard
import { ExecutionContext, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

export class JwtCognitoGuard extends AuthGuard('jwtCognito') {
  constructor() {
    super();
  }
}
Auth.controller
// 省略
  @UseGuards(JwtCognitoGuard)
  @Post('/login')
  async login(
    @Body() requestBody: LoginWithUserAuthRequestBody,
  ): Promise<MyAuthenticationResponse> {
    // 処理
  }

ConfigServiceは何だって人はこちら
https://zenn.dev/dove/articles/2990f0e1eba07e

困ること

AuthGuardを使うと、Strategy内部のエラーが握りつぶされることがあります。エラーにUnauthorizedという情報しか返さなくて、どのようなエラーが発生したのかわからないのです。

@nestjs/passportauth.guard.jsの実装は以下の様になっており、errが存在するか、userがfalseになればエラーが投げられます。しかし、エラーの中には、infoにエラー内容が入っているものもあり、その際はエラー詳細がここで握りつぶされてしまいます。

@nestjs/passport auth.guard.js
        handleRequest(err, user, info, context, status) {
            if (err || !user) {
                throw err || new common_1.UnauthorizedException();
            }
            return user;
        }

実際に僕がハマったエラーは、JwtCognitoStrategyで、環境変数を入れ忘れていたことで、jwksライブラリがそんなAPI存在しないよというものでした。

export class JwtCognitoStrategy extends PassportStrategy(
  Strategy,
  'jwtCognito',
) {
  private logger = new Logger(JwtCognitoStrategy.name);

  constructor(
    @Inject('AppConfigService')
    private configService: AppConfigService,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      algorithms: ['RS256'],
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://cognito-idp.${configService.get(
          'aws.region',
        )}.amazonaws.com/${configService.get(
          'cognito.userPoolId', // 👈ここ 環境変数入れ忘れてた
        )}/.well-known/jwks.json`,
      }),
      passReqToCallback: true,
    });
  }
}

このときhandleRequestには以下のような情報が渡されます。

handleRequest(null,false,{"message":"Bad Request","name":"JwksError"}, 省略)

第1引数がnullで、第2引数がfalseなので、これはエラーとして扱われます。しかし、エラーの内容が第3引数に入っています。

@nestjs/passportauth.guard.jsの実装は以下の様になっており、errが存在するか、userがfalseになればエラーが投げられます。しかし、エラーの中には、infoにエラー内容が入っているものもあり、その際はエラー詳細がここで握りつぶされてしまいます。

@nestjs/passport auth.guard.js
        handleRequest(err, user, info, context, status) {
            if (err || !user) {
                throw err || new common_1.UnauthorizedException();
            }
            return user;
        }

そこで、この時点でログをとっておくことで、エラー内容を拾うことができます。独自のAuthGuardhandleRequestを定義して、そこでログを取ります。そのあと親のメソッドに渡してあげます。

AuthGuard
import { ExecutionContext, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

export class JwtCognitoGuard extends AuthGuard('jwtCognito') {
  private logger = new Logger('JwtCognitoGuard');

  constructor() {
    super();
  }

  // 👇 追記
  handleRequest(
    ...args: Parameters<
      ReturnType<typeof AuthGuard>['prototype']['handleRequest']
    >
  ) {
    // エラーだけログに記録
    // エラーとは、第1引数(err)があるか、第2引数(user)がfalseの場合
    if (args[0] || !args[1]) {
      this.logger.error(args);
    }
    return super.handleRequest(
      args[0],
      args[1],
      args[2],
      args[3] as any,
      args[4],
    );
  }
}

こうすることで、無事に401エラーのデバッグができました!

TRAPE(トラピ)

Discussion