NestJS を使ってわかったことを書いていく
NestJS の CSRF 対応
NestJS の CSRF 対応は、基本的に(Express or Fastify の)ライブラリを使う。
ただし、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 対応が可能。
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 に対してライブラリを適用しているので、ライブラリが効かないことも納得できる。