├── .editorconfig ├── package.json ├── LICENSE ├── .gitignore ├── README.md └── lib └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | charset = utf-8 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-firebase-auth", 3 | "version": "0.2.0", 4 | "description": "Hapi.js auth strategy using Firebase access tokens", 5 | "main": "lib/index.js", 6 | "repository": "https://github.com/gabrielreiscom/hapi-firebase-auth", 7 | "author": "Gabriel Reis ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "hapi": "^17.2.0" 11 | }, 12 | "dependencies": { 13 | "boom": "^7.1.1", 14 | "firebase-admin": "^5.10.0" 15 | }, 16 | "peerDependencies": { 17 | "hapi": ">= 17" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gabriel Reis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Hapi-Firebase Auth 3 |

4 | 5 | ## Overview 6 | 7 | If you have an App using Firebase Auth and need to connect them with your backend API, this is the plugin for you. 8 | 9 | This auth strategy verify the token sent in the request and only grant access to valid tokens. Invalid tokens will get a `401 - Unauthorized` response. 10 | 11 | ## Features 12 | 13 | * Compatible with Hapi v17 14 | * Firebase Admin initializer and loader 15 | * Gluten-free 16 | 17 | ## Instalation 18 | 19 | ### Using NPM 20 | 21 | ``` 22 | npm install hapi-firebase-auth --save 23 | ``` 24 | 25 | ### Using Yarn 26 | 27 | ``` 28 | yarn add hapi-firebase-auth 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Step 1 - Add auth strategy 34 | 35 | #### Using a new Firebase Admin instance 36 | 37 | In case you don't want to initialize Firebase Admin externally, pass your Firebase credentials using the property `credential` as shown below. This way the plugin will handle it for you. 38 | 39 | ```js 40 | // Load Hapi-Firebase Auth Strategy 41 | const HapiFirebaseAuth = require('hapi-firebase-auth'); 42 | 43 | // Register the plugin 44 | await server.register({ 45 | plugin: HapiFirebaseAuth 46 | }); 47 | 48 | // Include auth strategy 49 | server.auth.strategy('firebase', 'firebase', { 50 | credential: { 51 | projectId: '', 52 | clientEmail: 'foo@.iam.gserviceaccount.com', 53 | privateKey: '-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n', 54 | databaseURL: 'https://.firebaseio.com' 55 | } 56 | }) 57 | ``` 58 | 59 | You can get the credentials for your project in your Firebase Console. More details here. 60 | 61 | 62 | #### Using a pre-existing Firebase Admin instance 63 | 64 | If there is already an existing Firebase Admin instance, pass it using the property `instance` as shown below. 65 | 66 | ```js 67 | // Load Hapi-Firebase Auth Strategy 68 | const HapiFirebaseAuth = require('hapi-firebase-auth'); 69 | 70 | // Initialize the default app 71 | const admin = require('firebase-admin'); 72 | 73 | // Register the plugin 74 | await server.register({ 75 | plugin: HapiFirebaseAuth 76 | }); 77 | 78 | // Initialize the Admin SDK with your credentials 79 | // This is an example of what it should look in your code 80 | admin.initializeApp({ 81 | credential: admin.credential.cert({ 82 | projectId: '', 83 | clientEmail: 'foo@.iam.gserviceaccount.com', 84 | privateKey: '-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n' 85 | }), 86 | databaseURL: 'https://.firebaseio.com' 87 | }); 88 | 89 | // Include auth strategy using existing Firebase Admin instance 90 | server.auth.strategy('firebase', 'firebase', { 91 | instance: admin 92 | }) 93 | ``` 94 | 95 | If you are having issues with Firebase Admin SDK, click here and make sure all your credentials are correct. 96 | 97 | ### Step 2 - Setup routes 98 | 99 | Add property `auth` with value `firebase` to the `config` object in the routes you want to protect. 100 | 101 | ```js 102 | server.route({ 103 | method: 'GET', 104 | path: '/', 105 | config: { 106 | auth: 'firebase' 107 | }, 108 | handler(request, reply) { 109 | return "Can't touch this!" 110 | } 111 | }); 112 | ``` 113 | 114 | ### Step 3 - Test requests 115 | 116 | Send requests to the protected endpoints using the `authorization` header: 117 | 118 | ``` 119 | Authorization: Bearer ey3tn03g2no5ig0gimt9gwglkrg0495gj(...) 120 | ``` 121 | 122 | * If the provided token is `VALID`, the endpoint will be accessible as usual. 123 | * If the provided token is `INVALID` or `EXPIRED`, a `401 - Unauthorized` error will be returned. 124 | 125 | ## Error codes 126 | 127 | | Code | Description | 128 | |-------------------------------------|-------------------------------------------------------| 129 | | `token_not_provided` | `Authorization` header with `Bearer` keyword not found| 130 | | `auth_provider_not_initialized` | Firebase Admin was not initialized properly (check your credentials) | 131 | | `invalid_token` | The token is not valid. It could also be expired. | 132 | 133 | ## Support 134 | 135 | 24/7 customer service available. You can find the number for your area on the back of this page. 136 | 137 | ## Contribute 138 | 139 | Contribuitions are welcome and highly encouraged! This is a simple plugin but we can always make it better ;) 140 | 141 | 142 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Load required modules 4 | const FirebaseAdminSdk = require('firebase-admin'); 5 | const Package = require('../package'); 6 | const Unauthorized = require('boom').unauthorized; 7 | 8 | function firebaseAuthScheme(server, options) { 9 | return { 10 | 11 | /** 12 | * Hapi's authenticate method. This is where the magic happens 13 | * 14 | * @param {*} request Request object created by the server 15 | * @param {*} h Standard hapi response toolkit 16 | */ 17 | authenticate(request, h) { 18 | 19 | // Get token from header 20 | const token = getToken(request); 21 | 22 | // If token not found, return an 'unauthorized' response 23 | if (token === null) { 24 | throw Unauthorized('token_not_provided'); 25 | } 26 | 27 | // This variable will hold Firebase's instance 28 | let firebaseInstance; 29 | 30 | try { 31 | 32 | if (options.instance) { 33 | 34 | // Great! We can just use this instance ready to go 35 | firebaseInstance = options.instance; 36 | 37 | } else { 38 | 39 | // Initialize Firebase instance 40 | firebaseInstance = FirebaseAdminSdk.initializeApp({ 41 | credential: FirebaseAdminSdk.credential.cert({ 42 | projectId: options.credential.projectId, 43 | clientEmail: options.credential.clientEmail, 44 | privateKey: options.credential.privateKey 45 | }), 46 | databaseURL: options.credential.databaseURL 47 | }) 48 | } 49 | 50 | // Check if Firebase is initialized 51 | const instance = firebaseInstance.instanceId(); 52 | 53 | } catch (error) { 54 | 55 | // Firebase not initialized 56 | throw Unauthorized('auth_provider_not_initialized'); 57 | } 58 | 59 | // Validate token 60 | return validateToken(token, firebaseInstance, h); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Gets the token from the request header 67 | * This function looks for the property 'authorization' in the 68 | * header and gets whatever is appended after the word 'Bearer' 69 | * 70 | * @param {*} request Request object created by the server 71 | */ 72 | function getToken(request) { 73 | 74 | // Get authorization property from request header 75 | const requestAuthorization = request.headers.authorization; 76 | if (!requestAuthorization) return null; 77 | 78 | // Define a regular expression to match the case we want and test it 79 | const matchRegex = new RegExp(/(bearer)[ ]+(.*)/i); 80 | const resultMatch = requestAuthorization.match(matchRegex); 81 | 82 | // If no matches found, there is no token available 83 | if (!resultMatch) return null; 84 | 85 | // Match found! Return token 86 | return resultMatch[2] || null; 87 | } 88 | 89 | /** 90 | * Validate the provided token using Firebase Admin SDK 91 | * 92 | * @param {*} token Access token provided by Firebase Auth 93 | * @param {*} firebaseInstance Initialized Firebase App instance 94 | * @param {*} h Standard hapi response toolkit 95 | */ 96 | function validateToken(token, firebaseInstance, h) { 97 | 98 | // Verify token using Firebase's credentials 99 | return firebaseInstance.auth().verifyIdToken(token) 100 | .then(function(credentials) { 101 | 102 | // Valid token! 103 | return h.authenticated({ credentials }) 104 | 105 | }).catch(function(error) { 106 | 107 | console.log(error) 108 | 109 | // Invalid token 110 | throw Unauthorized('invalid_token') 111 | }); 112 | } 113 | 114 | function register (server, options) { 115 | 116 | server.auth.scheme('firebase', firebaseAuthScheme); 117 | 118 | // decorate the request with a "user" property 119 | // this passes responsibility to hapi that 120 | // no other plugin uses "request.user" 121 | server.decorate('request', 'user', undefined); 122 | 123 | server.ext('onPostAuth', (request, h) => { 124 | 125 | // user successfully authenticated? 126 | if (request.auth.credentials) { 127 | // assign user object to request by using its credentials 128 | request.user = { 129 | email: request.auth.credentials.email || null, 130 | id: request.auth.credentials.uid || null, 131 | name: request.auth.credentials.name || null, 132 | user_id: request.auth.credentials.user_id || null, 133 | username: request.auth.credentials.email || null 134 | } 135 | } 136 | 137 | // continue request lifecycle 138 | return h.continue 139 | }) 140 | } 141 | 142 | // Export plugin 143 | exports.plugin = { 144 | name: Package.name, 145 | version: Package.version, 146 | Package, 147 | register 148 | } 149 | --------------------------------------------------------------------------------