npm package: @hannesroth88/nestjs-firebase-auth
Description: NestJS Passport Strategy for Firebase Auth using Firebase Admin SDK, forked from tfarras/nestjs-firebase-auth
Author: Hannes Roth
Latest version: 2.0.3

#

<div align="center">
  <a href="http://nestjs.com/" target="_blank">
    <img src="https://nestjs.com/img/logo_text.svg" width="150" alt="Nest Logo" />
  </a>
</div>

<h3 align="center">NestJS Passport Strategy for Firebase Auth using Firebase Admin SDK</h3>

<div align="center">
  <a href="https://nestjs.com" target="_blank">
    <img src="https://img.shields.io/badge/built%20with-NestJs-red.svg" alt="Built with NestJS">
  </a>
</div>

# Installation

## Fork
This is a fork from tfarras/nestjs-firebase-auth. All credits to him.
I just updated dependencies and a bit of documentation.

## Install peer dependencies

```bash
npm install passport passport-jwt
npm install --save-dev @types/passport-jwt
```

## Install `@nestjs/passport` for authentication

```bash
npm install @nestjs/passport
```

## Install strategy

```bash
npm install @hannesroth88/nestjs-firebase-auth
```

# Important

To work with Firebase Auth you need to configure and initialize your firebase app. For this purpose you can use my [module for firebase-admin](https://github.com/tfarras/nestjs-firebase-admin).

# Create strategy

```typescript
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt } from 'passport-jwt';
import { FirebaseAuthStrategy } from '@tfarras/nestjs-firebase-auth';

@Injectable()
export class FirebaseStrategy extends PassportStrategy(FirebaseAuthStrategy, 'firebase') {
  public constructor() {
    super({
      extractor: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });
  }
}
```

Note: You should provide an extractor. More information about passport-jwt extractors you can find here: [http://www.passportjs.org/packages/passport-jwt/#included-extractors](http://www.passportjs.org/packages/passport-jwt/#included-extractors)

# Create `AuthModule` and provide created strategy

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

@Module({
  imports: [PassportModule],
  providers: [FirebaseStrategy],
  exports: [FirebaseStrategy],
  controllers: [],
})
export class AuthModule { }
```

# Import `AuthModule` into `AppModule`

```typescript
import { FirebaseAdminCoreModule } from '@tfarras/nestjs-firebase-admin';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from 'nestjs-config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import * as path from 'path';

@Module({
  imports: [
    ConfigModule.load(path.resolve(__dirname, 'config', '**', '!(*.d).{ts,js}')),
    FirebaseAdminCoreModule.forRootAsync({
      useFactory: (config: ConfigService) => config.get('firebase'),
      inject: [ConfigService],
    }),
    AuthModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule { }

```

# Protect your routes

```typescript
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { FirebaseAdminSDK, FIREBASE_ADMIN_INJECT } from '@tfarras/nestjs-firebase-admin';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject(FIREBASE_ADMIN_INJECT) private readonly fireSDK: FirebaseAdminSDK,
  ) { }

  @Get()
  @UseGuards(AuthGuard('firebase'))
  getHello() {
    return this.fireSDK.auth().listUsers();
  }
}

```

# Custom second validation

In cases when you want to validate also if user exists in your database, or anything else after successfull Firebase validation you can define custom `validate` method in your strategy.

## Example

```typescript
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { FirebaseAuthStrategy, FirebaseUser } from '@tfarras/nestjs-firebase-auth';
import { ExtractJwt } from 'passport-jwt';

@Injectable()
export class FirebaseStrategy extends PassportStrategy(FirebaseAuthStrategy, 'firebase') {
  public constructor() {
    super({
      extractor: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });
  }

  async validate(payload: FirebaseUser): Promise<FirebaseUser> {
    // Do here whatever you want and return your user
    return payload;
  }
}
```


# Add Role requirement

In cases when you want to implement a Role Based Authorization you can do sth like this:

## Controller

```typescript
import {
  Controller,
  Get,
  Param,
  Put,
  Query,
  UseGuards,
} from '@nestjs/common';
import { UserService } from './user.service';
import { UserRecord } from 'firebase-admin/lib/auth/user-record';
import { Role } from 'src/auth/roles/roles.model';
import { RolesOneOf } from 'src/auth/roles/roles.decorator';
import { FirebaseAuthGuard } from 'src/auth/jwt.guard';
import { UserRolesDto } from './dto/user-role-update.dto';
import { ListUsersResult } from 'firebase-admin/lib/auth/base-auth';

@UseGuards(FirebaseAuthGuard)
@RolesOneOf(Role.ADMIN)
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Get()
  async listUsers(): Promise<ListUsersResult> {
    return this.userService.listUsers();
  }

  @Get(':id')
  async getUser(@Param('id') userId: string): Promise<UserRecord> {
    return await this.userService.getUser(userId);
  }

  @Put(':id/roles')
  async updateUserRoles(
    @Param('id') userId: string,
    @Query() query: UserRolesDto,
  ): Promise<void> {
    await this.userService.updateUserRoles(userId, query.rolesToUpdate);
  }
}
```

## AuthGuard
```typescript
import { ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { FirebaseJwtPayload } from './firebase/firebase.types';
import { Role } from './roles/roles.model';

@Injectable()
export class FirebaseAuthGuard extends AuthGuard('firebase') {
  private readonly logger = new Logger(FirebaseAuthGuard.name);
  constructor(
    private reflector: Reflector,
  ) {
    super();
  }
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // call AuthGuard in order to ensure user is injected in request
    const baseGuardResult = await super.canActivate(context);
    if (!baseGuardResult) {
      // unsuccessful authentication return false
      this.logger.error('baseGuardResult is null');
      return false;
    }

    // successfull authentication, user is injected
    const { user } = context.switchToHttp().getRequest();
    const jwt = user as FirebaseJwtPayload;

    const requireRolesOneOf = this.reflector.getAllAndOverride<Role[]>('rolesOneOf', [context.getHandler(), context.getClass()]);
    if (requireRolesOneOf) {
      const hasRole = requireRolesOneOf.some((role) => jwt.roles.includes(role));
      if (!hasRole) {
        this.logger.error(`User ${jwt.user_id} does not have any of the required roles: ${requireRolesOneOf.join(', ')}`);
      }
      return hasRole;
    } else {
      return true;
    }
  }
}
```

## Role Decorator

```typescript
import { SetMetadata } from '@nestjs/common';

export const RolesOneOf = (...roles: string[]) => SetMetadata('rolesOneOf', roles);

export enum Role {
    ADMIN = 'ADMIN',
    GUEST = 'GUEST',
  }
```

## User Service
```typescript
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { ListUsersResult } from 'firebase-admin/lib/auth/base-auth';
import * as admin from 'firebase-admin';
import { UserRecord } from 'firebase-admin/lib/auth/user-record';

@Injectable()
export class UserService {
  constructor() {}

  async getUser(userId: string): Promise<UserRecord> {
    try {
      return await admin.auth().getUser(userId);
    } catch (error) {
      console.error('Error getting user:', error);
      throw new InternalServerErrorException('Error getting user');
    }
  }

  async listUsers(): Promise<ListUsersResult> {
    try {
      return await admin.auth().listUsers();
    } catch (error) {
      console.error('Error getting users:', error);
      throw new InternalServerErrorException('Error getting users');
    }
  }

  async updateUserRoles(uid: string, roles: string[]): Promise<void> {
    try {
      await admin.auth().setCustomUserClaims(uid, { roles });
    } catch (error) {
      console.error('Error updating user roles:', error);
      throw new InternalServerErrorException('Error updating user roles');
    }
  }
}
```