NestJS와cognito를 통해 JWT 인증을 실현한 샘플

네스트JS와 JWT 인증을 통해 조사한 결과 자신이 JWT를 발행하는 사람이 많고, 코그니토 등 외부에서 기호화폐를 발행하는 시스템의 인증 샘플이 적어 공유됐다.
공식 사이트에서 인증에 관한 페이지는 다음과 같은 링크입니다.
https://docs.nestjs.com/security/authentication

목표


다음은 app 컨트롤러에 jwt 인증을 추가합니다.이 컨트롤러에 요청을 보내도 Authentication 헤더에 올바른 JWT를 추가하지 않으면 401 오류로 팝업됩니다.
app.controller.ts
@UseGuards(JwtGuard)
@Controller()
export class AppController {

  @Get('hello')
  testGet(): string {
    return 'Hello!';
  }
}

설치하다.


먼저 다음을 설치하십시오.
 $ npm install --save @nestjs/passport passport jwks-rsa passport-jwt
 $ npm install --save-dev @types/passport-jwt
@nestjs/passport는 NestJS에서 여권 모듈을 처리하기 위한 것이다.jwks-rsacognito 서버에서 키를 가져오고 서명이 있는지 확인하는 데 사용됩니다.이곳 자체도 설치할 수 있으니 잠시 후 이 라이브러리를 사용하지 않는 방법을 소개하겠습니다.passport-jwt JWT 정책을 작성하는 데 사용됩니다.

만든 물건


이번에 만든 것은 주로 세 가지가 있다.
  • JWT 보호
  • passport에 대응하는 JWT 정책
  • auth모듈
  • NestJS의 방어에 대해 잘 모르시는 분들은 이 공식 홈페이지의 해설을 읽어보시는 것을 추천합니다.
    https://docs.nestjs.com/guards
    디렉토리 구성은 다음과 같습니다.
    .
    ├──app.module.ts
    ├──app.controller.ts
    └──auth
       ├──auth.module.ts
       ├──guard
       │  └──jwt.guard.ts
       └──strategy
          └──jwt.strategy.ts
    
    하나하나 설명하고 싶습니다.

    app.module.ts


    루트 부분의 모듈입니다.이번에는 jwt 인증 코드가 없습니다.
    app.module.ts
    import {
      Module,
      NestModule,
      MiddlewareConsumer,
      RequestMethod,
    } from '@nestjs/common';
    
    import { AuthModule } from './auth/auth.module';
    import { AppController } from './app.controller';
    
    @Module({
      imports: [AuthModule],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    

    app.controller.ts


    루트 부분의 컨트롤러.이 컨트롤러에 대해 JWT 인증을 하고 싶습니다.
    app.controller.ts
    import {
      Controller,
      Get,
      UseGuards,
    } from '@nestjs/common';
    import { JwtGuard } from './auth/guard/jwt.guard';
    
    // JWT認証をこのコントローラー全体に適応
    @UseGuards(JwtGuard)
    @Controller()
    export class AppController {
    
      @Get('hello')
      testGet(): string {
        return 'Hello!';
      }
    }
    

    auth/auth.module.ts


    드디어 메인 메뉴네요.auth 모듈.NestJS를 위한 맞춤형 NodeJS 대표 인증 라이브러리Passport.js@nestjs/passport를 사용합니다.
    auth.module.ts
    import { Module } from '@nestjs/common';
    import { PassportModule } from '@nestjs/passport';
    
    import { JwtStrategy } from './strategy/jwt.strategy';
    // パターン2を使いたいときはコメントインする。
    // import { ManualJwtStrategy } from './strategy/jwt.manual.strategy';
    
    @Module({
      imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
      ],
      providers: [
        JwtStrategy,
        // ManualJwtStrategy 
      ],
    })
    export class AuthModule {}
    

    auth/guard/jwt.guard.ts


    NestJS를 JWT처럼 보호합니다.컨트롤러@UseGuards로 인테리어와 함께 사용하면 무효 인증을 받을 수 있다.
    auth/guard/jwt.guard.ts
    import { AuthGuard } from '@nestjs/passport';
    
    // AuthGuard('jwt')の引数の値は
    // jwt.strategy.tsのPassportStrategy(Strategy, 'jwt')に合わせる。
    export class JwtGuard extends AuthGuard('jwt') {
      constructor() {
        super();
      }
    }
    

    [모드 1 라이브러리 검증]auth/stratgy/jwt.strategy.ts


    는 JWT 인증 엔티티입니다.passport-jwtStrategy는 구조기가 지정한 secretOrKeyProvider에 따라 JWT를 검사한다.유효한 JWTvalidate만 디코딩하여 드립니다.잘못된 JWT 오류가 401개 반환됩니다.
    그런데 왜 401 오류에 대한 상세한 일지를 남기지 않았는지 영패가 좋지 않았는지, 다른 실시 방식이 좋지 않았는지 모르겠다.이런 상황에서 아래 모델 2의 실시 방식을 통해 디버깅이 더욱 쉬울 수 있다.
    auth/strategy/jwt.strategy.ts
    import {
      Injectable,
      Logger,
    } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { passportJwtSecret } from 'jwks-rsa';
    import { ExtractJwt, Strategy } from 'passport-jwt';
    
    export interface Claim {
      sub: string;
      email: string;
      token_use: string;
      auth_time: number;
      iss: string;
      exp: number;
      username: string;
      client_id: string;
    }
    
    const COGNITO_CLIENT_ID = 'hogehoge';
    const COGNITO_REGION = 'ap-northeast-1';
    const COGNITO_USERPOOL_ID = 'fugafuga';
    
    const AUTHORITY = `https://cognito-idp.${COGNITO_REGION}.amazonaws.com/${COGNITO_USERPOOL_ID}`;
    
    // PassportStrategy(Strategy, 'jwt')部分の第一引数は
    // jwt.guard.tsでのAuthGuard('jwt')に合わせる。
    @Injectable()
    export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
      private logger = new Logger(JwtStrategy.name);
    
      constructor() {
        super({
          // ヘッダからBearerトークンを取得
          jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
          ignoreExpiration: false,
          // cognitoのクライアントidを指定
          audience: COGNITO_CLIENT_ID,
          // jwt発行者。今回はcognito
          issuer: AUTHORITY,
          algorithms: ['RS256'],
          // もし自分がjwt発行してるなら秘密鍵を指定するが、
          // cognitoなど外部サービスが発行してるならsecretOrKeyProviderを利用。
          secretOrKeyProvider: passportJwtSecret({
            // 公開鍵をキャッシュする。これがfalseだと、毎リクエストごとに
    	// 公開鍵をHTTPリクエストで取得する必要がある。
            cache: true,
            rateLimit: true,
            jwksRequestsPerMinute: 5,
            jwksUri: `${AUTHORITY}/.well-known/jwks.json`,
          }),
          // passReqToCallback: true, // これをtrueにするとvalidateの第一引数にRequestを使用できる。
        });
      }
    
      // JWT検証後、デコードされたpayloadを渡してくる。
      // 検証後に実行されることに注意。JWTが向こうであればそもそも実行されない。
      // validate自体はPromiseにすることも可能。
      public validate(payload: Claim): string {
        return payload.email;
      }
    }
    
    

    [모드2 자체 검증]auth/stratgy/jwt.strategy.manual.ts


    프로그램 라이브러리의 힘을 빌리지 않고 스스로 JWT 검증을 하는 방법.
    먼저 다음을 설치하십시오.
     $ npm install --save passport-custom jwk-to-pem jsonwebtoken
     $ npm install --save-dev @types/jsonwebtoken @types/jwk-to-pem
    
    auth/strategy/jwt.strategy.manual.ts
    import {
      UnauthorizedException,
      Injectable,
      Logger,
    } from '@nestjs/common';
    import { PassportStrategy } from '@nestjs/passport';
    import { Request } from 'express';
    import { Strategy } from 'passport-custom'; // passport-jwtでないので注意!
    
    import { handler } from './jwt.verify';
    
    // もしこれを利用したい場合は
    // jwt.guard.tsのAuthGuard('jwt')を
    // AuthGuard('manualJwt')にすること。
    @Injectable()
    export class ManualJwtStrategy extends PassportStrategy(Strategy, 'manualJwt') {
      private logger = new Logger(ManualJwtStrategy.name);
    
      constructor() {
        super();
      }
    
      public async validate(req: Request): Promise<string> {
        const info = await handler(req.get('Authorization'));
        if (!info.isValid) throw new UnauthorizedException(info.error);
    
        return info.email;
      }
    }
    
    제가'passport-jwt'를 잘 하도록 노력하겠습니다Strategy.하는 일
  • 리퀘스트의 Authentication 헤더에서 영패를 받습니다.그때는 아주 가벼운 문자로 발리를 세었다.
  • cognito 서버에서 공개 키를 받습니다.
  • 공개 키를 사용하여 jwt의 서명이 있는지 확인
  • 오류가 발생하면 오류 내용이 반환됩니다.
  • 가 있으면 디코딩된 내용을 되돌려줍니다.
  • 네.나는 어떤 보도의 코드를 참고했지만 참고원을 잊어버렸다.
    그리고 이걸 쓰고 이런 도서관을 발견했어요.
    https://github.com/awslabs/aws-jwt-verify
    auth/strategy/jwt.verify.ts
    /* eslint-disable camelcase */
    import { UnauthorizedException, Logger } from '@nestjs/common';
    import { Request, Response, NextFunction } from 'express';
    import jwt from 'jsonwebtoken';
    import jwkToPem from 'jwk-to-pem';
    import request from 'request';
    
    const logger = new Logger('jwt.verify');
    
    // 参考 https://github.com/awslabs/aws-support-tools/blob/master/Cognito/decode-verify-jwt/decode-verify-jwt.ts
    
    const TOKEN_USE_ACCESS = 'access';
    const TOKEN_USE_ID = 'id';
    
    const ALLOWED_TOKEN_USES = [TOKEN_USE_ACCESS, TOKEN_USE_ID];
    const ISSUER = `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`;
    
    export interface ClaimVerifyRequest {
      readonly token?: string;
    }
    
    export interface ClaimVerifyResult {
      readonly email: string;
      readonly clientId: string;
      readonly isValid: boolean;
      readonly error?: any;
    }
    
    interface TokenHeader {
      kid: string;
      alg: string;
    }
    interface PublicKey {
      alg: string;
      e: string;
      kid: string;
      kty: 'RSA'; // サンプルはstringだったがjwtToPemの型で弾かれるため、こうした
      n: string;
      use: string;
    }
    interface PublicKeyMeta {
      instance: PublicKey;
      pem: string;
    }
    
    interface PublicKeys {
      keys: PublicKey[];
    }
    
    interface MapOfKidToPublicKey {
      [key: string]: PublicKeyMeta;
    }
    
    export interface Claim {
      sub: string;
      email: string;
      token_use: string;
      auth_time: number;
      iss: string;
      exp: number;
      username: string;
      client_id: string;
      'cognito:username': string;
    }
    
    /** レスポンスの例
    "sub": "aaaaaaaa-bbbb-cccc-dddd-example",
    "aud": "xxxxxxxxxxxxexample",
    "email_verified": true,
    "token_use": "id",
    "auth_time": 1500009400,
    "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/your_userpool_id",
    "cognito:username": "HATOSUKE",
    "exp": 1500013000,
    "given_name": "HATOSUKE",
    "iat": 1500009400,
    "email": "[email protected]"
    */
    
    let cacheKeys: MapOfKidToPublicKey | undefined;
    // One time initialisation to download the JWK keys and convert to PEM format. Returns a promise.
    const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
      if (!cacheKeys) {
        const options = {
          url: `${ISSUER}/.well-known/jwks.json`,
          json: true,
        };
        const publicKeys = await new Promise<PublicKeys>((resolve, reject) => {
          request.get(options, function (err, resp, body: PublicKeys) {
            if (err) {
              logger.debug(`Failed to download JWKS data. err: ${err}`);
              reject(new Error('Internal debug occurred downloading JWKS data.')); // don't return detailed info to the caller
    
              return;
            }
            if (!body || !body.keys) {
              logger.debug(
                `JWKS data is not in expected format. Response was: ${JSON.stringify(
                  resp,
                )}`,
              );
              reject(new Error('Internal debug occurred downloading JWKS data.')); // don't return detailed info to the caller
    
              return;
            }
    
            resolve(body);
          });
        });
    
        cacheKeys = publicKeys.keys.reduce((agg, current) => {
          const pem = jwkToPem(current);
          agg[current.kid] = { instance: current, pem };
    
          return agg;
        }, {} as MapOfKidToPublicKey);
    
        return cacheKeys;
      }
    
      return cacheKeys;
    };
    
    // Verify the Authorization header and return a promise.
    function verifyProm(keys: MapOfKidToPublicKey, token?: string): Promise<Claim> {
      return new Promise((resolve, reject) => {
        // Decode the JWT token so we can match it to a key to verify it against
    
        if (!token || token.length < 2) {
          reject(
            new UnauthorizedException(
              "Invalid or missing Authorization header. Expected to be in the format 'Bearer <your_JWT_token>'.",
            ),
          );
    
          return;
        }
    
        const decodedNotVerified = jwt.decode(token, {
          complete: true,
        }) as { header: TokenHeader } | null;
    
        if (!decodedNotVerified) {
          logger.debug('Invalid JWT token. jwt.decode() failure.');
          reject(
            new UnauthorizedException(
              'Authorization header contains an invalid JWT token.',
            ),
          ); // don't return detailed info to the caller
    
          return;
        }
    
        if (
          !decodedNotVerified.header.kid ||
          !keys[decodedNotVerified.header.kid]
        ) {
          logger.debug(
            `Invalid JWT token. Expected a known KID ${JSON.stringify(
              Object.keys(keys),
            )} but found ${decodedNotVerified.header.kid}.`,
          );
          reject(
            new UnauthorizedException(
              'Authorization header contains an invalid JWT token.',
            ),
          ); // don't return detailed info to the caller
    
          return;
        }
    
        const key = keys[decodedNotVerified.header.kid];
    
        // Now verify the JWT signature matches the relevant key
        jwt.verify(
          token,
          key.pem,
          {
            issuer: ISSUER,
          },
          function (err, decodedAndVerified: any) {
            if (err) {
              logger.debug(`Invalid JWT token. jwt.verify() failed: ${err}.`);
              if (err instanceof jwt.TokenExpiredError) {
                reject(
                  new UnauthorizedException(
                    `Authorization header contains a JWT token that expired at ${err.expiredAt.toISOString()}.`,
                  ),
                );
              } else {
                reject(
                  new UnauthorizedException(
                    'Authorization header contains an invalid JWT token.',
                  ),
                ); // don't return detailed info to the caller
              }
    
              return;
            }
    
            // The signature matches so we know the JWT token came from our Cognito instance, now just verify the remaining claims in the token
    
            // Verify the token_use matches what we've been configured to allow
            if (ALLOWED_TOKEN_USES.indexOf(decodedAndVerified.token_use) === -1) {
              logger.debug(
                `Invalid JWT token. Expected token_use to be ${JSON.stringify(
                  ALLOWED_TOKEN_USES,
                )} but found ${decodedAndVerified.token_use}.`,
              );
              reject(
                new UnauthorizedException(
                  'Authorization header contains an invalid JWT token.',
                ),
              ); // don't return detailed info to the caller
    
              return;
            }
    
            // Verify the client id matches what we expect. Will be in either the aud or the client_id claim depending on whether it's an id or access token.
            const clientId = decodedAndVerified.aud || decodedAndVerified.client_id;
            if (clientId !== process.env.COGNITO_CLIENT_ID) {
              logger.debug(
                `Invalid JWT token. Expected client id to be ${process.env.COGNITO_CLIENT_ID} but found ${clientId}.`,
              );
              reject(
                new UnauthorizedException(
                  'Authorization header contains an invalid JWT token.',
                ),
              ); // don't return detailed info to the caller
    
              return;
            }
    
            // Done - all JWT token claims can now be trusted
            resolve(decodedAndVerified as Claim);
          },
        );
      });
    }
    
    // Verify the Authorization header and call the next middleware handler if appropriate
    function verifyMiddleWare(
      pemsDownloadProm: Promise<MapOfKidToPublicKey>,
      token?: string,
    ) {
      return pemsDownloadProm
        .then((keys) => {
          return verifyProm(keys, token);
        })
        .then((decoded: Claim) => {
          // Caller is authorised - copy some useful attributes into the req object for later use
          logger.debug(`Valid JWT token. Decoded: ${JSON.stringify(decoded)}.`);
    
          return {
            email: decoded.email,
            clientId: decoded.client_id,
            isValid: true,
          };
        })
        .catch((err: any) => {
          logger.debug(String(err));
    
          return { email: '', clientId: '', error: err, isValid: false };
        });
    }
    
    // Get the middleware function that will verify the incoming request
    const handler = async (auth?: string): Promise<ClaimVerifyResult> => {
      // Fetch the JWKS data used to verify the signature of incoming JWT tokens
    
      // Check the format of the auth header string and break out the JWT token part
      if (!auth || auth.length < 10) {
        throw new UnauthorizedException(
          "Invalid or missing Authorization header. Expected to be in the format 'Bearer <your_JWT_token>'.",
        );
      }
    
      const authPrefix = auth.substring(0, 7).toLowerCase();
      if (authPrefix !== 'bearer ') {
        throw new UnauthorizedException(
          "Authorization header is expected to be in the format 'Bearer <your_JWT_token>'.",
        );
      }
    
      const token = auth.substring(7);
    
      const pemsDownloadProm = getPublicKeys().catch((err) => {
        // Failed to get the JWKS data - all subsequent auth requests will fail
        logger.debug(err);
    
        return { err };
      });
    
      return verifyMiddleWare(pemsDownloadProm, token);
    };
    
    export { handler };
    
    

    좋은 웹페이지 즐겨찾기