NestJSでGraphQLのQueryまたはMutationをGuardする方法を説明します。
基本的には、NestJSの公式に掲載されていますが、GraphQLをGuardするには少し工夫が必要です。
環境
- NestJS 6.0
ゴール
NestJSでGraphQLをGuardする方法はいくつかあります。
今回は、ユーザのロールによってGuardする方法を採用します。
以下のような感じでGaurdします。@nestjs/passportのjwtでGuardした上で、ユーザのロールが2となっているもののみアクセスできるmutationを実装します(queryでも同じです)
@Mutation(returns => SaveSiteConfigResult)
@UseGuards(JwtAuthGuard)
@Roles(2)
savesiteconfig(@Args('savesiteconfig') _saveSiteConfig: SaveSiteConfig): Promise<SaveSiteConfigResult> {
Gaurdの実装
まずは、Guardの実装です。
詳しくはコメントに記載しましたが、ざっくりとは以下の通りです。
- SetMetadataでGuard定義されているロール値を取得する
- ロール値が取得できない場合は、Guard定義されていないと言うことになるので、trueを返す
- authorizationヘッダーからJWTトークンを取得を取得し公開鍵で検証する
- JWTトークンのpayloadに設定されているロール値がGuard定義されているロール値に該当するかをチェックする
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import * as jwt from 'jsonwebtoken';
import { IJwtPayload } from '../auth/interfaces/ijwt-payload.interface';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private readonly reflector: Reflector
) {}
canActivate(context: ExecutionContext): Promise<boolean> {
return new Promise((resolve, reject) => {
// SetMetadataのrolesに定義されたロール値を取得する
const roles = this.reflector.get<number[]>('roles', context.getHandler());
// ロール値が取得できなければtrue(つまり、ガードされていない)
if (!roles) {
resolve(true);
}
else {
// contextからGraphQLのctxを取得する
// GraphQLのforRootで「context: ({ req }) => ({ req }),」の定義が必要
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
// authorizationヘッダーからJWTトークンを取得する
const authToken = req.headers.authorization;
const jwtToken = authToken.substring(authToken.indexOf(' ') + 1);
// JWTトークンを公開鍵で検証する
jwt.verify(jwtToken, Buffer.from(process.env.RSA_PUBLIC_KEY, 'base64'), (err, payload: IJwtPayload) => {
// JWTトークンの検証ができなかったらエラー
if (err) {
reject(err);
}
else {
// ユーザのロールがGuardで設定されているロールに合致するかを検証する
resolve(roles.includes(payload.role));
}
});
}
});
}
}
GraphQLを使う時のポイントは、上記のコードの以下の部分です(これ以外は公式に記載のもので実現可能です)
// contextからGraphQLのctxを取得する
// GraphQLのforRootで「context: ({ req }) => ({ req }),」の定義が必要
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
Decoratorの定義
上のものでGuardするには、@SetMetadata('roles', 1)
とする必要があります。
これでもいいのですが、もう少し分かりやすくするように、Decoratorを定義します。これで、@Roles(1)
と書くことができます。
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: number[]) => SetMetadata('roles', roles);
グローバルなGuardとして定義
app.modules.tsで作成したGuardをグローバルGuardとして定義します。
これで、全てのGraphQLで作成したGuardがコールされるようになります。
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './guards/rolesguard';
@Module({
providers: [
{ provide: APP_GUARD, useClass: RolesGuard }
],
})