nestjs + graphql + で firebase auth token認証

● nestjs + graphql + で firebase auth token認証

引用 : https://bit.ly/3K7o4x5

● firebase-admin SDK を追加する

・firebase-adminの インストール

npm install firebase
npm install firebase-admin

・firebase-adminの設定

src/main.ts の bootstrap() に以下を追加する

firebaseコンソールからサービスアカウントのjsonファイルをダウンロードしておきます(コンソール→プロジェクトの設定→サービスアカウント→新しい秘密鍵を生成)

import * as admin from 'firebase-admin';
import * as serviceAccount from '/PATH/TO/YOUR/SERVICE/ACCOUNT.json';


async function bootstrap() {
  // firebase-admin
  const params = {
    type: serviceAccount.type,
    projectId: serviceAccount.project_id,
    privateKeyId: serviceAccount.private_key_id,
    privateKey: serviceAccount.private_key,
    clientEmail: serviceAccount.client_email,
    clientId: serviceAccount.client_id,
    authUri: serviceAccount.auth_uri,
    tokenUri: serviceAccount.token_uri,
    authProviderX509CertUrl: serviceAccount.auth_provider_x509_cert_url,
    clientC509CertUrl: serviceAccount.client_x509_cert_url,
  };
  admin.initializeApp({
    credential: admin.credential.cert(params),
  });

A. @nestjs/passport を使用して認証機能を作成する場合

● guard を作成する

・Nest.jsにおけるGuardとは?

Nest.jsにおけるGuardは実際には次の条件を全て満たすようなものです:
 ・クラスである
 ・@Injectable()デコレーターでアノテーションされている
 ・CanActivateというインターフェースをimplementsしている
 ・canActivateという、ExecutionContext型を引数にとり、同期または非同期でboolean値(trueまたはfalse)を返すメソッドを実装している

引用 : https://bit.ly/41kceXx

src/auth/guard/auth.guard.ts

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";
import { AuthService } from "../auth.service";
import { GqlExecutionContext } from "@nestjs/graphql";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const requestHeaders = ctx.getContext().req.headers;
    if (!requestHeaders) {
      throw new Error("ヘッダが正しく設定されていません。");
    }
    const idToken: string = requestHeaders.authorization.replace("Bearer ", "");
    try {
      const user = await this.authService.validateUser(idToken);
      ctx.getContext().req["user"] = user;
      return user !== undefined;
    } catch (error) {
      throw new UnauthorizedException("認証情報が正しくありません。");
    }
  }
}

「CanActivate」 は 結果の返却は、Booleanのため、情報の再利用はできません。
「AuthGuard」を 使用すると結果の返却は Object | Boolean となるため 情報の再利用が可能となります

● guard をリゾルバに適用させる

・モジュールで読み込み

src/users/users.module.ts

import { AuthService } from '../auth/auth.service';

// AuthService を追加します
@Module({
  providers: [UsersResolver, UsersService, AuthService],
})

・リゾルバに @UseGuards(AuthGuard) を追加

  @Query(() => User, { name: 'user' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.usersService.findOne(id);
  }

  ↓

メソッドのレベルで使用するために、次のように @UseGuards() デコレータを使用します

import { UseGuards } from '@nestjs/common';

@UseGuards(AuthGuard)
  @Query(() => User, { name: 'user' })
  findOne(@Args('id', { type: () => Int }) id: number) {
    return this.usersService.findOne(id);
  }

以上です。

B. @nestjs/passport を使用して認証機能を作成する場合

https://docs.nestjs.com/recipes/passport

@nestjs/passport の インストール

npm install --save @nestjs/passport passport passport-jwt passport-http-bearer

以下の「ストラテジ」「モジュール」「ガード」を作成して @UseGuards(FirebaseAuthGuard) という記述でガードを使用できるようにします。

src/auth/firebase.strategy.ts

import {
  HttpException,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-http-bearer';
import { DecodedIdToken } from 'firebase-admin/lib/auth';
import * as admin from 'firebase-admin';

@Injectable()
export class FirebaseStrategy extends PassportStrategy(Strategy, 'firebase') {
  constructor() {
    super();
  }

  async validate(idToken: string): Promise<DecodedIdToken> {
    if (!idToken) {
      throw new UnauthorizedException('認証が必要です。');
    }

    try {
      return await admin.auth().verifyIdToken(idToken);
    } catch (error) {
      throw new HttpException('Forbidden', error);
    }
  }
}

src/auth/firebase/firebase.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { FirebaseStrategy } from './firebase.strategy';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'firebase' })],
  providers: [FirebaseStrategy],
  exports: [PassportModule],
})
export class FirebaseModule {}

src/auth/firebase/firebaseAuth.guard.ts

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class FirebaseAuthGuard extends AuthGuard('firebase') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().req;
  }
}

使い方は同じです。 ガードを加えます。

  @Query(() => User, { name: 'user' })

  ↓

  @UseGuards(FirebaseAuthGuard)
  @Query(() => User, { name: 'user' })

参考 : https://hi1280.hatenablog.com/

● ガードでfirebaseトークン検証時に取り出したユーザーの値を再利用する

デコレーターを作成し、そこから取り出します。

src/auth/firebase/current-user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';

export type CurrentFirebaseUserData = {
  uid: string;
  name: string;
  email: string;
};

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    const requestUser = ctx.getContext().req['user'];
    const currentUser: CurrentFirebaseUserData = {
      uid: requestUser.uid,
      name: requestUser.name,
      email: requestUser.email,
    };
    return currentUser;
  },
);

使い方

  @UseGuards(FirebaseAuthGuard)
  @Query(() => User, { name: 'profile' })
  profile(@CurrentUser() user: CurrentFirebaseUserData) {
    return this.usersService.findOneFromEmail(user.email);
  }

このようにして profile メソッドに user を渡すことができます。

graphql クエリ例

{
  profile{
    id,name,email,authProvider,authId,createdAt,updatedAt
  }
No.2311
12/18 08:42

edit