This is a fork from tfarras/nestjs-firebase-auth. All credits to him. I just updated dependencies and a bit of documentation.
npm install passport passport-jwt
npm install --save-dev @types/passport-jwt
npm install @nestjs/passport
npm install @hannesroth88/nestjs-firebase-auth
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.
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
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 { 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 { }
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();
}
}
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.
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;
}
}
In cases when you want to implement a Role Based Authorization you can do sth like this:
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);
}
}
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;
}
}
}
import { SetMetadata } from '@nestjs/common';
export const RolesOneOf = (...roles: string[]) => SetMetadata('rolesOneOf', roles);
export enum Role {
ADMIN = 'ADMIN',
GUEST = 'GUEST',
}
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');
}
}
}