Open2

NestJS を使ってわかったことを書いていく

KodakKodak

NestJS の CSRF 対応

NestJS の CSRF 対応は、基本的に(Express or Fastify の)ライブラリを使う。

https://docs.nestjs.com/security/csrf

ただし、Express or Fastify のライブラリのすべての機能がそのまま NestJS で使えるとは限らない。

たとえば、Fastify の CSRF ライブラリ('@fastify/csrf-protection')を使ってみる。

  • Fastify の場合
import Fastify from 'fastify';
import fastifyCsrf from '@fastify/csrf-protection';

const fastify = Fastify({ logger: true });
fastify.register(fastifyCsrf);
fastify.csrfProtection; // csrfProtectionメソッドが使用可能。
  • NestJS の場合
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';

const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({ logger: true }),
);
await app.register(fastifyCsrf);
app.csrfProtection; // 使えない。。。。

なぜか?

おそらく、NestJS の 1 つ 1 つの機能は、疎結合で実装するから。

機能ごとの依存関係はモジュールを用いて解決する必要がある。

このルールに合わないようなライブラリの機能はおそらく使えない(のだと思う)。

以下は、@fastify/csrf-protectionのコードの抜粋。

おそらくすべて(Express or Fastify の)ライブラリに言えることだが、decorateで呼び出すことのできる機能は使用できないだろう。。。

if (sessionPlugin === '@fastify/secure-session') {
  fastify.decorateReply('generateCsrf', generateCsrfSecureSession); // <= 使用可
} else if (sessionPlugin === '@fastify/session') {
  fastify.decorateReply('generateCsrf', generateCsrfSession); // <= 使用可
} else {
  fastify.decorateReply('generateCsrf', generateCsrfCookie); // <= 使用可
}

fastify.decorate('csrfProtection', csrfProtection); // <= 使用不可

ではどうするのか?

generateCsrfは使って、csrfProtectionは自前で実装する。

generateCsrfは以下のようにそのまま使う。

// app.service.ts
export class AppService {
  async confirm(rep: FastifyReply) {
    return {
      // generateCsrf を使ってCsrfTokenを生成して、Responseを返す。
      // これを使うと、デフォルトだとCookieにも`_csrf`というKey名でトークンを埋め込まれる。
      token: rep.generateCsrf({
        httpOnly: true,
        secure: true,
      }),
    };
  }
}

csrfProtectionは以下のように自前で実装する。

実装箇所は、Guard が適切なような気がした。

// csrf.guard.ts
import CSRF from '@fastify/csrf';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { FastifyRequest } from 'fastify';

@Injectable()
export class CsrfGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<FastifyRequest>();
    const secret = request.cookies['_csrf'];

    if (!secret) {
      return false;
    }

    const tokens = new CSRF();
    // CookieのCSRF Token(自動埋め込み)と、Requestから取得したCSRF Token(こちらはBodyやheaders等から取得)を検証して、問題なければ`true`にする
    if (!tokens.verify(secret, getToken(request), getUserInfo(request))) {
      return false;
    }

    function getToken(req) {
      return (
        (req.body && req.body._csrf) ||
        req.headers['csrf-token'] ||
        req.headers['xsrf-token'] ||
        req.headers['x-csrf-token'] ||
        req.headers['x-xsrf-token']
      );
    }

    function getUserInfo(req) {
      return undefined;
    }

    return true;
  }
}

これで、CSRF 対応が可能。

KodakKodak

NestJS の Middleware がライブラリを認識しないことがある

こちらは現在、原因不明なので、事象だけ記載しておく。

@fastify/cookieを使って、Guard と Middleware で Request の Cookie がパースできているか確認する。

ライブラリは、registerを使って読み込んでおく。

import fastifyCookie from '@fastify/cookie';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
    {
      bufferLogs: true,
    },
  );

  await app.register(fastifyCookie);
}
  • Middleware
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyReply, FastifyRequest } from 'fastify';

@Injectable()
export class CsrfMiddleware implements NestMiddleware {
  use(req: FastifyRequest, reply: FastifyReply, next: () => void) {
    console.log('middleware', req.cookies);
    next();
  }
}
  • Guard
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { HttpExceptionResponse } from 'src/types/http-response';

@Injectable()
export class CsrfGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<FastifyRequest>();
    console.log('guard', request.cookies);
    return true;
  }
}
  • 結果
middleware undefined
guard { _csrf: 'xxxx' }

Middleware は、Cookie のパースができておらず、ライブラリが効いていない。

原因は、今のところ謎。

もしかすると、Middleware は NestJS の機能ではなく、Express or Fastify の機能をそのまま持ってきているだけなのかもしれない。。。

であれば、NestJS に対してライブラリを適用しているので、ライブラリが効かないことも納得できる。