タイトルのとおりですが、NestJSのGraphQL利用時に、Firebase Authenticationの idToken
を使って認証する実装方法について考えました。
前提
以下のようなデータベーステーブルを定義しています。
// Firebaseアカウントと同期されるテーブル model Account { id String @default(uuid()) @id firebaseAuthUid String @unique // Firebase Authenticationのuidをセットする email String? @unique user User? @@index([firebaseAuthUid]) } model User { id String @default(uuid()) @id name String tasks Task[] account Account @relation(fields: [accountId], references: [id]) accountId String @unique }
- まずはfirebase-adminを初期化
bootstrap時に初期化を行っておきます。
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { initializeFirebaseAdmin } from './_initializers/initialize-firebase-admin'; import { LoggerService } from './common/logger/logger.service'; import { ServiceAccount, getApps, initializeApp } from 'firebase-admin/app'; import { credential } from 'firebase-admin'; function initializeFirebaseAdmin() { const cert: ServiceAccount = { projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), }; if (getApps().length === 0) { initializeApp({ credential: credential.cert(cert), }); } } async function bootstrap() { initializeFirebaseAdmin(); const app = await NestFactory.create(AppModule, { logger: new LoggerService(), }); await app.listen(3002); } bootstrap();
- 次にFirebase Authenticationで取得できるidTokenをAuthorizationヘッダから受け取りdecodeするためのMiddlewareを書きます。
MiddlewareはNestJSがルーティングを行う前に処理を挿入でき、Requestを拡張することができます。
これを使って、RequestからAuthorization headerを読み取り、idTokenを取得してDBからユーザーレコードを引っ張り、Requestに詰めてあげます。
import { Injectable, NestMiddleware } from '@nestjs/common'; import { NextFunction, Response } from 'express'; import { getAuth } from 'firebase-admin/auth'; import { RequestWithCurrentUser } from '../interfaces/request-with-current-user'; import { AccountRepository } from '../repositories/account.repository'; @Injectable() export class FirebaseAuthMiddleware implements NestMiddleware { constructor(private readonly accountRepository: AccountRepository) {} async use(req: RequestWithCurrentUser, _: Response, next: NextFunction) { const { authorization } = req.headers; req.currentFirebaseUser = null; if (authorization) { const [scheme, token] = req.headers.authorization?.split(' ') ?? []; if (scheme === 'Bearer') { const decodedIdToken = await getAuth().verifyIdToken(token); const currentFirebaseUser = { uid: decodedIdToken.uid, email: decodedIdToken.email ?? null, }; const account = await this.accountRepository.findOrCreateByFirebaseAuthUid( currentFirebaseUser, ); req.currentFirebaseUser = currentFirebaseUser; req.currentAccount = account; req.currentUser = account.user ?? null; } } next(); } }
- Middlewareを登録する
FirebaseAuthMiddleware
を利用するためにAppModuleに設定を書きます。
ルーティング毎に設定できますが、今回は /graphql
のみ反映できれば良いですが、他にルートもないのでforRoutes('*')
に設定しています。
// ...略 export class AppModule { configure(consumer: MiddlewareConsumer) { consumer.apply(FirebaseAuthMiddleware).forRoutes('*'); } }
- 現在のユーザーを取得するデコレータを作成する
import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { RequestWithCurrentUser } from '../interfaces/request-with-current-user'; import { User } from '~/user/entities/user.entity'; export type CurrentUser = User | null; export const CurrentUser = createParamDecorator( (_data: unknown, ctx: ExecutionContext): CurrentUser => { const gqlContext = GqlExecutionContext.create(ctx); const req: RequestWithCurrentUser = gqlContext.getContext().req; return req.currentUser; }, );
- 作ったデコレータを利用して、現在のユーザーを取得するには以下のようにします。
import { CurrentUser } from '~/auth/current-user.decorator'; @ResolveField(() => [Task], { nullable: false }) async tasks(@CurrentUser() user: CurrentUser) { if (user == null) return []; const tasks = await this.taskRepository.findMany({ ownerId: user.id, }); return tasks; }
これでResolveFieldやMutationなどで、CurrentUserを手軽に扱えるようになります
今回載せたコードを含むサンプルはこちらにあります。