🐈‍⬛

NestJSのGraphQLでFirebase Authenticationでシンプルな認証するのにちょっと詰まった件

2023/11/08に公開

はじめに

個人開発でNestJSとGraphQLを使ってみたくて、素人ながらやってみたら認証の実装にちょっと迷ったので備忘録として残しておきます。
NestJS、GraphQLエキスパートな人なら当然のベストプラクティスがあるのかもしれませんが、自分は迷ったので同じようなことする方の参考になれば…!

やりたかったこと

  • 全部のクエリとかミューテーションとか叩く時に、Firebase Authenticationからもらったtokenをヘッダーに渡したい。
  • 下記みたいによくある感じで。
"Authorization": "Bearer {TOKEN}"
  • なかったら弾いて、あったらデコードしたい。

当初やろうとしたこと(middlewareでやる)

  • middlewareを作る
firebase-auth.middleware.ts
import {
  Injectable,
  NestMiddleware,
  UnauthorizedException,
} from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import * as admin from "firebase-admin";

@Injectable()
export class FirebaseAuthMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const authorization = req.headers.authorization;

    if (!authorization) {
      throw new UnauthorizedException("No authorization token provided");
    }

    try {
      const token = authorization.replace("Bearer ", "");
      const decodedToken = await admin.auth().verifyIdToken(token);
      req["user"] = decodedToken;
      next();
    } catch (error) {
      throw new UnauthorizedException("Invalid ID token");
    }
  }
}

  • app.module.tsでapplyする。
app.module.ts
import { FirebaseAuthMiddleware } from "./common/middlewares/firebase-auth.middleware";
--- 略 ---
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(FirebaseAuthMiddleware).forRoutes("*");
  }
}

問題

これだと、ほんとに全部のパスでミドルウェアがかかるので、GraphQLのプレイグラウンドすら弾いてしまう、、、
プレイグラウンド

consumer.apply(FirebaseAuthMiddleware).exclude(〜)

みたいな除外するパスを指定することもできるようだが、/graphqlを弾くわけにもいかないのでどうしたものか…

結局(Guardでやる)

ガードを使うことに。

  • ガード作成
auth.guard.ts
import {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";
import * as admin from "firebase-admin";

@Injectable()
export class AuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const request = ctx.getContext().req;
    const authorization = request.headers.authorization;

    // ヘッダーにauthorizationがない場合は認証失敗
    if (!authorization) {
      return false;
    }

    // トークンを検証
    const user = await this.validateToken(authorization);

    // ユーザー情報をリクエストに追加
    request.user = user;

    return true;
  }

  private async validateToken(authorization: string) {
    try {
      const token = authorization.replace("Bearer ", "");
      const decodedToken = await admin.auth().verifyIdToken(token);
      return decodedToken;
    } catch (error) {
      throw new UnauthorizedException(error);
    }
  }
}

  • アノテーション(@UseGuards)を追加
hoge.resolver.ts
--- 略 ---
import { AuthGuard } from "src/common/guards/auth.guard";
import { UseGuards } from "@nestjs/common";
--- 略 ---
@Resolver((of) => Hoge)
export class HogeResolver {
  constructor(private readonly hogeService: HogeService) {}

  @Query((returns) => [Hoge])
  // ここでアノテーション追加
  @UseGuards(AuthGuard)
  hoges(@Context() context): Promise<Hoge[]> {
    const user = context.req.user;
    return this.hogeService.findListByUid(user.uid);
  }
}

終わりに

これでプレイグラウンドは使えて、アノテーション追加したクエリ等を叩いた時のみガードしてくれる。

が、当初は全部に一括でかけたくてミドルウェアでやるのかな…?と思って実装してた節もあり、このやり方だと全部にガードのアノテーション付与していく必要があり、素人にはちょっと疑問が残る…
そんなもんなんですかね?

以上、誰かの参考になれば〜

Discussion