タイトルのとおりですが、Next.jsの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を手軽に扱えるようになります
今回載せたコードを含むサンプルはこちらにあります。