🧑⚖️
Nest.js × GraphQL に Firebase Auth で Authentication / Authorization を導入
概要
自社サーバーを書き直すにあたり、Nest.jsでFirebase Authを使用したのでメモ
要件
- クエリを叩く際に
Authorization
ヘッダーにBearer tokenを渡す - tokenからユーザーのroleを取得
- GraphQLのクエリ単位でroleごとのアクセス権限を設定
- sdkはFirebase Adminを使用
Nest.js + GraphQL
GraphQLが叩ける環境はできているものとする
initializeApp
admin.initializeApp
を叩くと、Firebaseが初期化されます。
src/app.ts
import { NestFactory } from '@nestjs/core';
import admin from 'firebase-admin';
import { AppModule } from './app.module';
import * as serviceAccount from 'path/to/credential.json';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
});
await app.listen(8000);
}
bootstrap();
tsconfigのresolveJsonModuleを true
にしておくと良いです。
Middleware
requestの Authorization
ヘッダーからtokenを取り出し、verifyします。
今回はFirebase AuthのCustom Claimにrole情報を保存していますが、MiddlewareにはDIもできるのでuserIdを用いて別のDBから取得するなど、お好きにユーザー情報を取得してください。
src/auth/auth.middleware.ts
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { NextFunction, Request, Response } from 'express';
import { ICurrentUser } from './current-user.interface';
import admin from 'firebase-admin';
export type CustomRequest = Request & { user: ICurrentUser };
/** FirebaseLoginをしてユーザー情報をcontextに持たせるMiddleware */
@Injectable()
export class FirebaseAuthMiddleware implements NestMiddleware {
async use(req: CustomRequest, _: Response, next: NextFunction) {
const { authorization } = req.headers;
if (authorization) {
const token = authorization.slice(7); // remove "Bearer "
req.user = await admin
.auth()
.verifyIdToken(token)
.then((data) => ({
id: data.uid,
email: data.email ?? '',
role: data.role, // custom claim
}))
.catch((err) => {
throw new HttpException(
{ message: 'Token validation failed', err },
HttpStatus.UNAUTHORIZED,
);
});
}
next();
}
}
ちなみに実装は以下を大いに参考にしました。
AuthModule
AuthModule
を作ります。
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { FirebaseAuthMiddleware } from './auth.middleware';
@Module({
providers: [FirebaseAuthMiddleware],
imports: [],
exports: [FirebaseAuthMiddleware],
})
export class AuthModule {}
Resolverのあるmoduleにimportし、middlewareを consumer
に apply
します。
src/graphql/graphql.module.ts
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { AuthModule } from '../../auth/auth.module';
@Module({
imports: [UseCaseModule, AuthModule],
providers: [
HumanResolver,
],
})
export class GraphqlModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(FirebaseAuthMiddleware).forRoutes('/');
}
}
Guard
src/auth/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Reflector } from '@nestjs/core';
import { RoleType } from './roles.decolator';
import { ICurrentUser } from './current-user.interface';
/** GraphQLを叩く権限のGuard
* 実際に使う際は `@Roles` を用いる */
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const roles = this.reflector.get<RoleType[]>('roles', context.getHandler());
const ctx = GqlExecutionContext.create(context);
const req = ctx.getContext().req;
if (!roles) return true;
try {
const user: ICurrentUser = req.user;
return roles.includes(user.role as RoleType);
} catch (error) {
throw new UnauthorizedException('Invalid user');
}
}
}
書きやすくするためのCustom Decoratorを定義します。
src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
const roles = ['ADMIN', 'DEVELOPER', 'END_USER'] as const;
export type RoleType = (typeof roles)[number];
/** GraphQLを叩く権限のGuard
* Example:
* ```
* @Resolver(() => Human)
* export class HumanResolver {
* @Roles('ADMIN', 'DEVELOPER')
* @Mutation(() => Human)
* async createHuman() {}
* ```
* */
export const Roles = (...roles: RoleType[]) => SetMetadata('roles', roles);
実装は以下を大いに参考にしました。
あとは適宜importします。
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { FirebaseAuthMiddleware } from './auth.middleware';
import { RolesGuard } from './auth.guard';
@Module({
providers: [FirebaseAuthMiddleware, RolesGuard],
imports: [],
exports: [FirebaseAuthMiddleware, RolesGuard],
})
export class AuthModule {}
src/graphql/graphql.module.ts
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { AuthModule } from '../../auth/auth.module';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from '../../auth/auth.guard';
@Module({
imports: [UseCaseModule, AuthModule],
providers: [
HumanResolver,
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class GraphqlModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(FirebaseAuthMiddleware).forRoutes('/');
}
}
こんな感じで使えます。
src/graphql/human/human.resolver.ts
@Resolver(() => Human)
export class HumanResolver {
@Roles('ADMIN', 'DEVELOPER')
@Mutation(() => Human)
async createHuman() {...}
まとめ
- middlewareで認証を行い、contextにuserを突っ込んでおく
- guardの中で取り出して適切にauthorizeする
-
UseGuard
は decoratorでwrapしておくと便利
Discussion