├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── README.md ├── nest-cli.json ├── package.json ├── src ├── constants.ts ├── index.ts ├── interface │ ├── index.ts │ ├── object-literal.interface.ts │ └── options.interface.ts ├── passport-firebase.strategy.ts └── user.type.ts ├── tsconfig.build.json └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "arrowParens": "always", 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.0] - 2021-04-26 9 | 10 | ### Added 11 | - add checkRevoked option for jwt verification 12 | 13 | ### Changed 14 | 15 | - Updated NestJS dependencies 16 | - Updated `firebase-admin` dependecy 17 | - Updated scripts. Now it's using [Nest Cli](https://docs.nestjs.com/cli/overview) 18 | 19 | [2.0.0]: https://github.com/tfarras/nestjs-firebase-auth/releases/tag/2.0.0 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. [Fork it](https://help.github.com/articles/fork-a-repo/) 4 | 2. Install dependencies (`npm install`) 5 | 3. Create your feature branch (`git checkout -b my-new-feature`) 6 | 4. Commit your changes (`git commit -am 'Added some feature'`) 7 | 5. Push to the branch (`git push origin my-new-feature`) 8 | 6. [Create new Pull Request](https://help.github.com/articles/creating-a-pull-request/) 9 | 10 | ## Code Style 11 | 12 | We use [Prettier](https://prettier.io/) and eslint to maintain code style and best practices. 13 | Please make sure your PR adheres to the guides by running: 14 | 15 | ```bash 16 | npm run format 17 | ``` 18 | 19 | and 20 | 21 | ```bash 22 | npm run lint 23 | ``` 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 |
4 | 5 | Nest Logo 6 | 7 |
8 | 9 |

NestJS Passport Strategy for Firebase Auth using Firebase Admin SDK

10 | 11 |
12 | 13 | Built with NestJS 14 | 15 |
16 | 17 | # Installation 18 | 19 | ## Install peer dependencies 20 | 21 | ```bash 22 | npm install passport passport-jwt 23 | npm install --save-dev @types/passport-jwt 24 | ``` 25 | 26 | ## Install `@nestjs/passport` for authentication 27 | 28 | ```bash 29 | npm install @nestjs/passport 30 | ``` 31 | 32 | ## Install strategy 33 | 34 | ```bash 35 | npm install @tfarras/nestjs-firebase-auth 36 | ``` 37 | 38 | # Important 39 | 40 | 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). 41 | 42 | # Create strategy 43 | 44 | ```typescript 45 | import { Injectable } from "@nestjs/common"; 46 | import { PassportStrategy } from "@nestjs/passport"; 47 | import { ExtractJwt } from 'passport-jwt'; 48 | import { FirebaseAuthStrategy } from '@tfarras/nestjs-firebase-auth'; 49 | 50 | @Injectable() 51 | export class FirebaseStrategy extends PassportStrategy(FirebaseAuthStrategy, 'firebase') { 52 | public constructor() { 53 | super({ 54 | extractor: ExtractJwt.fromAuthHeaderAsBearerToken(), 55 | }); 56 | } 57 | } 58 | ``` 59 | 60 | 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) 61 | 62 | # Create `AuthModule` and provide created strategy 63 | 64 | ```typescript 65 | import { Module } from "@nestjs/common"; 66 | import { PassportModule } from "@nestjs/passport"; 67 | import { FirebaseStrategy } from "./firebase.strategy"; 68 | 69 | @Module({ 70 | imports: [PassportModule], 71 | providers: [FirebaseStrategy], 72 | exports: [FirebaseStrategy], 73 | controllers: [], 74 | }) 75 | export class AuthModule { } 76 | ``` 77 | 78 | # Import `AuthModule` into `AppModule` 79 | 80 | ```typescript 81 | import { FirebaseAdminCoreModule } from '@tfarras/nestjs-firebase-admin'; 82 | import { Module } from '@nestjs/common'; 83 | import { ConfigModule, ConfigService } from 'nestjs-config'; 84 | import { AppController } from './app.controller'; 85 | import { AppService } from './app.service'; 86 | import { AuthModule } from './auth/auth.module'; 87 | import * as path from 'path'; 88 | 89 | @Module({ 90 | imports: [ 91 | ConfigModule.load(path.resolve(__dirname, 'config', '**', '!(*.d).{ts,js}')), 92 | FirebaseAdminCoreModule.forRootAsync({ 93 | useFactory: (config: ConfigService) => config.get('firebase'), 94 | inject: [ConfigService], 95 | }), 96 | AuthModule, 97 | ], 98 | controllers: [AppController], 99 | providers: [AppService], 100 | }) 101 | export class AppModule { } 102 | 103 | ``` 104 | 105 | # Protect your routes 106 | 107 | ```typescript 108 | import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; 109 | import { AuthGuard } from '@nestjs/passport'; 110 | import { FirebaseAdminSDK, FIREBASE_ADMIN_INJECT } from '@tfarras/nestjs-firebase-admin'; 111 | import { AppService } from './app.service'; 112 | 113 | @Controller() 114 | export class AppController { 115 | constructor( 116 | private readonly appService: AppService, 117 | @Inject(FIREBASE_ADMIN_INJECT) private readonly fireSDK: FirebaseAdminSDK, 118 | ) { } 119 | 120 | @Get() 121 | @UseGuards(AuthGuard('firebase')) 122 | getHello() { 123 | return this.fireSDK.auth().listUsers(); 124 | } 125 | } 126 | 127 | ``` 128 | 129 | # Custom second validation 130 | 131 | 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. 132 | 133 | ## Example 134 | 135 | ```typescript 136 | import { Injectable } from "@nestjs/common"; 137 | import { PassportStrategy } from "@nestjs/passport"; 138 | import { FirebaseAuthStrategy, FirebaseUser } from '@tfarras/nestjs-firebase-auth'; 139 | import { ExtractJwt } from 'passport-jwt'; 140 | 141 | @Injectable() 142 | export class FirebaseStrategy extends PassportStrategy(FirebaseAuthStrategy, 'firebase') { 143 | public constructor() { 144 | super({ 145 | extractor: ExtractJwt.fromAuthHeaderAsBearerToken(), 146 | }); 147 | } 148 | 149 | async validate(payload: FirebaseUser): Promise { 150 | // Do here whatever you want and return your user 151 | return payload; 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tfarras/nestjs-firebase-auth", 3 | "version": "2.0.0", 4 | "description": "NestJS Passport Strategy for Firebase Auth using Firebase Admin SDK", 5 | "author": "Taimoor Farras ", 6 | "readmeFilename": "README.md", 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "files": [ 10 | "dist/**/*", 11 | "*.md" 12 | ], 13 | "scripts": { 14 | "prebuild": "rimraf dist", 15 | "build": "nest build", 16 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 17 | "start": "nest start", 18 | "start:dev": "nest start --watch", 19 | "start:debug": "nest start --debug --watch", 20 | "start:prod": "node dist/main", 21 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 26 | "test:e2e": "jest --config ./test/jest-e2e.json" 27 | }, 28 | "keywords": [ 29 | "nestjs", 30 | "firebase", 31 | "firebase-admin", 32 | "firebase-auth", 33 | "passport", 34 | "passport-jwt", 35 | "auth", 36 | "authentication" 37 | ], 38 | "publishConfig": { 39 | "access": "public" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/tfarras/nestjs-firebase-auth.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/tfarras/nestjs-firebase-auth/issues" 47 | }, 48 | "homepage": "https://github.com/tfarras/nestjs-firebase-auth#readme", 49 | "peerDependencies": { 50 | "@nestjs/common": "^7.6.15", 51 | "@types/passport-jwt": "^3.0.5", 52 | "firebase-admin": "^9.6.0", 53 | "passport": "^0.4.1", 54 | "passport-jwt": "^4.0.0" 55 | }, 56 | "devDependencies": { 57 | "@nestjs/cli": "^7.6.0", 58 | "@nestjs/common": "^7.6.15", 59 | "@nestjs/core": "^7.6.15", 60 | "@nestjs/platform-express": "^7.6.15", 61 | "@nestjs/schematics": "^7.3.0", 62 | "@nestjs/testing": "^7.6.15", 63 | "@types/express": "^4.17.11", 64 | "@types/jest": "^26.0.22", 65 | "@types/node": "^14.14.36", 66 | "@types/passport-jwt": "^3.0.5", 67 | "@types/supertest": "^2.0.10", 68 | "@typescript-eslint/eslint-plugin": "^4.19.0", 69 | "@typescript-eslint/parser": "^4.19.0", 70 | "eslint": "^7.22.0", 71 | "eslint-config-prettier": "^8.1.0", 72 | "eslint-plugin-prettier": "^3.3.1", 73 | "firebase-admin": "^9.6.0", 74 | "jest": "^26.6.3", 75 | "passport": "^0.4.1", 76 | "passport-jwt": "^4.0.0", 77 | "prettier": "^2.2.1", 78 | "reflect-metadata": "^0.1.13", 79 | "rimraf": "^3.0.2", 80 | "rxjs": "^6.6.6", 81 | "supertest": "^6.1.3", 82 | "ts-jest": "^26.5.4", 83 | "ts-loader": "^8.0.18", 84 | "ts-node": "^9.1.1", 85 | "tsconfig-paths": "^3.9.0", 86 | "typescript": "^4.2.3" 87 | }, 88 | "jest": { 89 | "moduleFileExtensions": [ 90 | "js", 91 | "json", 92 | "ts" 93 | ], 94 | "rootDir": "src", 95 | "testRegex": ".*\\.spec\\.ts$", 96 | "transform": { 97 | "^.+\\.(t|j)s$": "ts-jest" 98 | }, 99 | "collectCoverageFrom": [ 100 | "**/*.(t|j)s" 101 | ], 102 | "coverageDirectory": "../coverage", 103 | "testEnvironment": "node" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const UNAUTHORIZED = 'Unauthorized'; 2 | export const FIREBASE_AUTH = 'FIREBASE_AUTH'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './passport-firebase.strategy'; 3 | export * from './user.type'; 4 | export * from './interface'; 5 | -------------------------------------------------------------------------------- /src/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './object-literal.interface'; 2 | export * from './options.interface'; 3 | -------------------------------------------------------------------------------- /src/interface/object-literal.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ObjectLiteral { 2 | [key: string]: any; 3 | } 4 | -------------------------------------------------------------------------------- /src/interface/options.interface.ts: -------------------------------------------------------------------------------- 1 | import { JwtFromRequestFunction } from 'passport-jwt'; 2 | 3 | export interface FirebaseAuthStrategyOptions { 4 | extractor: JwtFromRequestFunction; 5 | checkRevoked?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/passport-firebase.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { JwtFromRequestFunction } from 'passport-jwt'; 3 | import { Strategy } from 'passport-strategy'; 4 | import { Request } from 'express'; 5 | import { FirebaseAuthStrategyOptions } from './interface/options.interface'; 6 | import { UNAUTHORIZED, FIREBASE_AUTH } from './constants'; 7 | import { FirebaseUser } from './user.type'; 8 | import * as admin from 'firebase-admin'; 9 | 10 | export class FirebaseAuthStrategy extends Strategy { 11 | readonly name = FIREBASE_AUTH; 12 | private checkRevoked = false; 13 | 14 | constructor( 15 | options: FirebaseAuthStrategyOptions, 16 | private extractor: JwtFromRequestFunction, 17 | private logger = new Logger(FirebaseAuthStrategy.name), 18 | ) { 19 | super(); 20 | 21 | if (!options.extractor) { 22 | throw new Error( 23 | '\n Extractor is not a function. You should provide an extractor. \n Read the docs: https://github.com/tfarras/nestjs-firebase-auth#readme', 24 | ); 25 | } 26 | 27 | this.extractor = options.extractor; 28 | this.checkRevoked = options.checkRevoked; 29 | } 30 | 31 | async validate(payload: FirebaseUser): Promise { 32 | return payload; 33 | } 34 | 35 | authenticate(req: Request): void { 36 | const idToken = this.extractor(req); 37 | 38 | if (!idToken) { 39 | this.fail(UNAUTHORIZED, 401); 40 | 41 | return; 42 | } 43 | 44 | try { 45 | admin 46 | .auth() 47 | .verifyIdToken(idToken, this.checkRevoked) 48 | .then((res) => this.validateDecodedIdToken(res)) 49 | .catch((err) => { 50 | this.fail({ err }, 401); 51 | }); 52 | } catch (e) { 53 | this.logger.error(e); 54 | 55 | this.fail(e, 401); 56 | } 57 | } 58 | 59 | private async validateDecodedIdToken(decodedIdToken: FirebaseUser) { 60 | const result = await this.validate(decodedIdToken); 61 | 62 | if (result) { 63 | this.success(result); 64 | } 65 | 66 | this.fail(UNAUTHORIZED, 401); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/user.type.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | 3 | export type FirebaseUser = admin.auth.DecodedIdToken; 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------