Nest.js:認証/認証

47202 ワード

しょきりゅう


  • 認証コードを電子メールで送信

  • 検証コード確認後、access token、refresh tokenをクッキーに入れて応答する
    *クッキーオプションを使用して、maxAgent、sameSite、secure、httponlyを設定します.

  • クライアントストレージのクッキーとともに要求

  • サーバはsecret keyを使用してrequest header cookieのtokenをチェックします

  • トークンは有効で、一致時に正常に応答し、一致しない時にエラーを送信します.

  • トークンが期限切れになった場合、refresh tokenを使用してアクセスtokenを再発行します.
  • *httpOnly
    このオプションを使用すると、JavaScriptなどのクライアント・スクリプトがCookieを無効にできます.  document.cookieではビスケットが見えず、ビスケットを操作することもできません.
    このオプションは、ハッカーが悪意のあるjavascriptコードをページに挿入し、ユーザーがログインを待つのを防止する場合に使用します.(CSS(Cross Site Scripting))
    ユーザーがWebページにアクセスするとき  document.cookieが表示され、操作可能なハッカーコードが一緒に実行されると、クッキーには認証情報があり、ハッカーがこれらの情報を盗んだり操作したりすることができます.
    でも.  httpOnly  オプション設定のCookie  document.cookieはクッキー情報を読み取ることができないので、クッキーを保護することができます.
    *secure
    セキュリティオプションが適用される場合、HTTPSプロトコルが使用されている場合にのみ転送されます.
    HTTP Only Cookieを使用すると、クライアントがJavaScriptを介してCookieを取得する問題を防止できますが、JavaScriptではなくネットワークを直接傍受してCookieをブロックすることもできます.
    これらの通信上の情報漏洩を防止するため,現在は主にHTTPSプロトコルを用いてデータを暗号化しているが,HTTPSを用いるとCookieも暗号化して伝送されるため,第三者は内容を知らない.
    *sameSite
    サーバは、サイト間リクエストと同時にCookieを送信すべきではないと主張し、サイト間リクエストの偽造攻撃の一部の保護を提供することができる.(CSRF)
      SameSite  オプションはCookie  Cross-Site  これは要求しない要求と一緒に伝える方法です.

    XSRF(cross-site request forgery, XSRF)



    現在  bank.comにログインしたと仮定すると、そのサイトで使用されている認証Cookieがブラウザに格納されます.  bank.comに要求が送信されるたびに、認証Cookieが一緒に送信される.サーバは、受信したCookieを使用してユーザーを識別し、セキュリティが必要な財務取引を処理します.
    今(ログアウトする必要はありません)別のウィンドウを開いてネットサーフィンを行います  evil.comに接続されています.このサイトにはハッカーの送金を要求するフォーム(フォーム)があります.  <form action="https://bank.com/pay">があり、フォームが自動コミットに設定されている場合、  evil.comから直接銀行サイトに送信すると、認証Cookieも一緒に送信されます.
      bank.comに要求が送信されるたびに  bank.comに設置されたクッキーが送信されたため、銀行は送信されたクッキー(ハッカーではなく)を読み取り、口座の所有者が登録したと考え、ハッカーにお金を送金します.

    Jwt Strategy,生成Jwt Guard

    //jwt.strategy.ts
    import { Injectable } from "@nestjs/common";
    import { ConfigService } from "@nestjs/config";
    import { PassportStrategy } from "@nestjs/passport";
    import cookie from "cookie";
    import { ExtractJwt, Strategy } from "passport-jwt";
    
    import { CustomError, ErrorCode } from "src/common/utils/error";
    import { AdminUserQueryService } from "src/admin-user/services/admin-user.query.service";
    
    export interface Payload {
      email: string;
      exp: number;
      iat: number;
      sub: string;
    }
    
    @Injectable()
    export class JtwStrategy extends PassportStrategy(Strategy) {
      constructor(
        readonly configService: ConfigService,
        private readonly adminUserQueryService: AdminUserQueryService
      ) {
        super({
          ignoreExpiration: false,
          jwtFromRequest: ExtractJwt.fromExtractors([
            (request) => {
              const cookies = request.headers.cookie;
    
              if (!cookies) {
                throw new CustomError(
                  "쿠키가 존재하지 않습니다.",
                  ErrorCode.COOKIE_DOES_NOT_EXIST
                );
              }
    
              const accessToken = cookie.parse(cookies).AccessToken;
              const refreshToken = cookie.parse(cookies).RefreshToken;
    
              if (!accessToken && !refreshToken) {
                throw new CustomError(
                  "토큰이 존재하지 않습니다.",
                  ErrorCode.ACCESS_AND_REFRESH_TOKEN_DOES_NOT_EXIST
                );
              }
    
              if (!accessToken) {
                throw new CustomError(
                  "토큰이 존재하지 않습니다.",
                  ErrorCode.ACCESS_TOKEN_DOES_NOT_EXIST
                );
              }
    
              return accessToken;
            },
          ]),
          secretOrKey: configService.get("JWT_ACCESS_TOKEN_SECRET"),
        });
      }
    
      async validate(payload: Payload) {
        const user = this.adminUserQueryService.getUserByEmail(
          payload.email
        );
    
        if (!user) {
          throw new CustomError(
            "사용자가 존재하지 않습니다.",
            ErrorCode.USER_DOES_NOT_EXIST
          );
        }
    
        return user;
      }
    }
    
    //payload
    {
      email: '[email protected]',
      sub: '14766dae-6c9c-41a0-91a6-4422817335d5',
      iat: 1631675933,
      exp: 1631679533
    }
    //jwt.guard.ts
    import { Injectable, ExecutionContext } from "@nestjs/common";
    import { Reflector } from "@nestjs/core";
    import { GqlExecutionContext } from "@nestjs/graphql";
    import { AuthGuard } from "@nestjs/passport";
    
    @Injectable()
    export class JwtAuthGuard extends AuthGuard("jwt") {
      constructor(private reflector: Reflector) {
        super();
      }
    
      getRequest(context: ExecutionContext) {
        const ctx = GqlExecutionContext.create(context);
    
        return ctx.getContext().req;
      }
    
      canActivate(context: ExecutionContext) {
        const isPublic = this.reflector.getAllAndOverride<boolean>(
          process.env["IS_PUBLIC_KEY"],
          [context.getHandler(), context.getClass()]
        );
    
        if (isPublic) {
          return true;
        }
    
        return super.canActivate(context);
      }
    }
    @Module({
      imports: [
        GraphQLModule.forRoot({
          autoSchemaFile: true,
          context: ({ req, res }) => makeContext({ res, req }),
          installSubscriptionHandlers: true,
          playground: !isProductionMode,
          formatError: (err: GraphQLError) => {
            if (err?.extensions?.exception && !err.extensions.exception.errorCode) {
              err.extensions.exception.errorCode = "SYSTEM_DEFAULT";
              err.extensions.exception.name = "SYSTEM_DEFAULT";
            }
    
            return err;
          },
          plugins,
          cors: {
            credentials: true,
            origin: true,
          },
        }),
        MailerModule.forRoot({
          transport: {
            SES: ses,
          },
          defaults: {
            from: "[email protected]",
          },
          template: {
            dir: "src/templates",
            adapter: new HandlebarsAdapter(),
            options: {
              strict: true,
            },
          },
        }),
        ConfigModule.forRoot({
          validationSchema: Joi.object({
            JWT_ACCESS_TOKEN_SECRET: Joi.string().required(),
            JWT_REFRESH_TOKEN_SECRET: Joi.string().required(),
            IS_PUBLIC_KEY: Joi.string().required(),
          }),
        }),
        ...Module,
      ],
      providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
    })
    export class AppModule {}
    //skip-auth.decorator.ts
    import { SetMetadata } from "@nestjs/common";
    
    export const Public = () => SetMetadata(process.env["IS_PUBLIC_KEY"], true);

    Jwt-reffreshポリシー、Jwt-reffresh guardの作成

    //jwt-refresh.strategy.ts
    import { Injectable } from "@nestjs/common";
    import { ConfigService } from "@nestjs/config";
    import { PassportStrategy } from "@nestjs/passport";
    import cookie from "cookie";
    import { ExtractJwt, Strategy } from "passport-jwt";
    
    import { Payload } from "src/auth/strategies/jwt.strategy";
    import { CustomError, ErrorCode } from "src/common/utils/error";
    import { RequestParam } from "src/context";
    import { AdminUserMutationService } from "src/admin-user/services/admin-user.mutation.service";
    
    @Injectable()
    export class JwtRefreshStrategy extends PassportStrategy(
      Strategy,
      "jwt-refresh-token"
    ) {
      constructor(
        private readonly adminUserMutationService: AdminUserMutationService,
        readonly configService: ConfigService
      ) {
        super({
          ignoreExpiration: false,
          jwtFromRequest: ExtractJwt.fromExtractors([
            (request) => {
              const cookies = request.headers.cookie;
    
              if (!cookies) {
                throw new CustomError(
                  "쿠키가 존재하지 않습니다.",
                  ErrorCode.COOKIE_DOES_NOT_EXIST
                );
              }
              const refreshToken = cookie.parse(cookies).RefreshToken;
    
              if (!refreshToken) {
                throw new CustomError(
                  "토큰이 존재하지 않습니다.",
                  ErrorCode.REFRESH_TOKEN_DOES_NOT_EXIST
                );
              }
              return refreshToken;
            },
          ]),
          passReqToCallback: true,
          secretOrKey: configService.get("JWT_REFRESH_TOKEN_SECRET"),
        });
      }
      async validate(req: RequestParam, payload: Payload) {
        const cookies = req.headers.cookie;
    
        if (!cookies) {
          throw new CustomError(
            "쿠키가 존재하지 않습니다.",
            ErrorCode.COOKIE_DOES_NOT_EXIST
          );
        }
    
        const refreshToken = cookie.parse(cookies)?.RefreshToken;
        const user = this.adminUserMutationService.validateRefreshToken({
          refreshToken: refreshToken!,
          userID: payload.sub,
        });
    
        return user;
      }
    }
    //jwt-refresh.guard.ts
    import { ExecutionContext, Injectable } from "@nestjs/common";
    import { GqlExecutionContext } from "@nestjs/graphql";
    import { AuthGuard } from "@nestjs/passport";
    
    @Injectable()
    export class JwtRefreshGuard extends AuthGuard("jwt-refresh-token") {
      constructor() {
        super();
      }
    
      getRequest(context: ExecutionContext) {
        const ctx = GqlExecutionContext.create(context);
        return ctx.getContext().req;
      }
    
      canActivate(context: ExecutionContext) {
        return super.canActivate(context);
      }
    }

    Resolverでの使用

    @Public()
      @UseGuards(JwtRefreshGuard)
      @Mutation(() => Boolean)
      async regenerateToken(
        @Context() ctx: IContext,
        @User() user: AdminUser
      ): Promise<Boolean> {
        const { accessToken, accessTokenOption } =
          await this.authMutationService.generateToken(user.email);
    
        ctx.res.cookie("AccessToken", accessToken, accessTokenOption);
    
        return true;
      }
    //user.decorator.ts
    import { createParamDecorator, ExecutionContext } from "@nestjs/common";
    import { GqlExecutionContext } from "@nestjs/graphql";
    
    export const User = createParamDecorator(
      (_, ctx: ExecutionContext) =>
        GqlExecutionContext.create(ctx).getContext().req.user
    );
    *質問
    現在、safariはcrossdomainクッキーセットにはなりません.
    *ソリューション
    ◆現在バックエンドで使用されているapiゲートウェイにカスタムドメインを追加します.リファレンス