├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── README.md ├── api ├── .gitkeep └── firebase-user │ ├── config │ └── routes.json │ ├── controllers │ └── firebase-user.js │ ├── models │ ├── firebase-user.js │ └── firebase-user.settings.json │ └── services │ └── firebase-user.js ├── config ├── database.js ├── functions │ ├── bootstrap.js │ ├── cron.js │ └── responses │ │ └── 404.js └── server.js ├── extensions ├── .gitkeep └── users-permissions │ └── config │ ├── jwt.js │ └── policies │ └── permissions.js ├── favicon.ico ├── package.json ├── public ├── robots.txt └── uploads │ └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=1337 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | build 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "browser": false 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": false 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "globals": { 18 | "strapi": true 19 | }, 20 | "rules": { 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "linebreak-style": ["error", "unix"], 23 | "no-console": 0, 24 | "quotes": ["error", "single"], 25 | "semi": ["error", "always"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | !public/uploads/.gitkeep 85 | 86 | ############################ 87 | # Node.js 88 | ############################ 89 | 90 | lib-cov 91 | lcov.info 92 | pids 93 | logs 94 | results 95 | node_modules 96 | .node_history 97 | 98 | 99 | ############################ 100 | # Tests 101 | ############################ 102 | 103 | testApp 104 | coverage 105 | 106 | ############################ 107 | # Strapi 108 | ############################ 109 | 110 | .env 111 | license.txt 112 | exports 113 | .cache 114 | build 115 | /serviceAccountKey.json 116 | /temp.js 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase Auth Implementation with Strapi 2 | 3 | Learn how to integrate firebase authentication into your strapi app. 4 | 5 | See `permissions.js` & `controllers/firebase-user.js` 6 | 7 | #### Install dependencies using NPM or Yarn 8 | 9 | `yarn install` 10 | 11 | #### Run Strapi Locally 12 | 13 | `yarn dev` or `yarn develop` 14 | 15 | [Read Medium Article](https://medium.com/@lambrohan/integrating-firebase-auth-into-strapi-9ed106ce8dee) 16 | 17 | [Create Owner's Policy](https://medium.com/@lambrohan/strapi-with-firebase-auth-owners-policy-f346a0457ffb) 18 | 19 | 20 | I hope this repo helped you with the strapi! 21 | 22 | If you can please consider [buying me a cup of coffee! ☕](https://www.buymeacoffee.com/lambrohan) 23 | -------------------------------------------------------------------------------- /api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambrohan/strapi-firebase-auth/3e08358ec1b20c62c07923bcfc011d46396c7ffc/api/.gitkeep -------------------------------------------------------------------------------- /api/firebase-user/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "method": "GET", 5 | "path": "/firebase-users", 6 | "handler": "firebase-user.find", 7 | "config": { 8 | "policies": [] 9 | } 10 | }, 11 | { 12 | "method": "GET", 13 | "path": "/firebase-users/count", 14 | "handler": "firebase-user.count", 15 | "config": { 16 | "policies": [] 17 | } 18 | }, 19 | { 20 | "method": "GET", 21 | "path": "/firebase-users/:id", 22 | "handler": "firebase-user.findOne", 23 | "config": { 24 | "policies": [] 25 | } 26 | }, 27 | { 28 | "method": "POST", 29 | "path": "/firebase-users", 30 | "handler": "firebase-user.create", 31 | "config": { 32 | "policies": [] 33 | } 34 | }, 35 | { 36 | "method": "PUT", 37 | "path": "/firebase-users/:id", 38 | "handler": "firebase-user.update", 39 | "config": { 40 | "policies": [] 41 | } 42 | }, 43 | { 44 | "method": "DELETE", 45 | "path": "/firebase-users/:id", 46 | "handler": "firebase-user.delete", 47 | "config": { 48 | "policies": [] 49 | } 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /api/firebase-user/controllers/firebase-user.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const { parseMultipartData, sanitizeEntity } = require("strapi-utils"); 3 | 4 | /** 5 | * Read the documentation (https://strapi.io/documentation/v3.x/concepts/controllers.html#core-controllers) 6 | * to customize this controller 7 | */ 8 | module.exports = { 9 | async create(ctx) { 10 | // create user from firebase token 11 | ctx.request.body.uid = ctx.state.user.uid; 12 | 13 | // uncomment the fields you have in content-type 14 | // ctx.request.body.email = ctx.state.user.email; 15 | // ctx.request.body.phoneNumber = ctx.state.user.phoneNumber; 16 | 17 | const entity = await strapi.services["firebase-user"].create( 18 | ctx.request.body 19 | ); 20 | await strapi.firebase 21 | .auth() 22 | .setCustomUserClaims(ctx.state.user.uid, { strapi_uid: entity.id }); 23 | return sanitizeEntity(entity, { model: strapi.models["firebase-user"] }); 24 | }, 25 | 26 | async update(ctx) { 27 | //update user while preventing the updates to user specific fields like uid, email 28 | const { id } = ctx.params; 29 | 30 | let entity; 31 | 32 | const user = ctx.state.user; 33 | 34 | if (!user) { 35 | return ctx.unauthorized(`You can't update this entry`); 36 | } 37 | 38 | if (id !== user.id) { 39 | return ctx.unauthorized(`You can't update this entry`); 40 | } 41 | 42 | if (ctx.is("multipart")) { 43 | let { data, files } = parseMultipartData(ctx); 44 | data.uid = user.uid; // a user must not update uid with req body (same goes for email also) 45 | // uncomment the fields you have in content-type 46 | // data.email = ctx.state.user.email; 47 | // data.phoneNumber = ctx.state.user.phoneNumber; 48 | entity = await strapi.services["firebase-user"].update({ id }, data, { 49 | files, 50 | }); 51 | } else { 52 | ctx.request.body.uid = user.uid; // a user must not update uid with req body (same goes for email also) 53 | // uncomment the fields you have in content-type 54 | // data.email = ctx.state.user.email; 55 | // data.phoneNumber = ctx.state.user.phoneNumber; 56 | entity = await strapi.services["firebase-user"].update( 57 | { id }, 58 | ctx.request.body 59 | ); 60 | } 61 | return sanitizeEntity(entity, { model: strapi.models["firebase-user"] }); 62 | }, 63 | 64 | /** 65 | * Return logged in user for GET request at localhost:1337/firebase-users 66 | * @param {Context} ctx 67 | */ 68 | async find(ctx) { 69 | const [entity] = await strapi.services["firebase-user"].find({ 70 | uid: ctx.state.user.uid, // find user associated with firebase uid 71 | }); 72 | return sanitizeEntity(entity, { model: strapi.models["firebase-user"] }); 73 | }, 74 | 75 | async findOne(ctx) { 76 | return ctx.badRequest("req not found"); 77 | }, 78 | 79 | /** 80 | * 81 | * NONE EXCEPT ADMIN CAN DELETE USER 82 | */ 83 | 84 | async delete(ctx) { 85 | ctx.badRequest("Bad Request"); 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /api/firebase-user/models/firebase-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Read the documentation (https://strapi.io/documentation/v3.x/concepts/models.html#lifecycle-hooks) 5 | * to customize this model 6 | */ 7 | 8 | module.exports = {}; 9 | -------------------------------------------------------------------------------- /api/firebase-user/models/firebase-user.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "firebase_users", 4 | "info": { 5 | "name": "Firebase User" 6 | }, 7 | "options": { 8 | "increments": true, 9 | "timestamps": true 10 | }, 11 | "attributes": { 12 | "uid": { 13 | "type": "string", 14 | "unique": true, 15 | "required": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/firebase-user/services/firebase-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Read the documentation (https://strapi.io/documentation/v3.x/concepts/services.html#core-services) 5 | * to customize this service 6 | */ 7 | 8 | module.exports = {}; 9 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | defaultConnection: 'default', 3 | connections: { 4 | default: { 5 | connector: 'mongoose', 6 | settings: { 7 | host: env('DATABASE_HOST', '127.0.0.1'), 8 | srv: env.bool('DATABASE_SRV', false), 9 | port: env.int('DATABASE_PORT', 27017), 10 | database: env('DATABASE_NAME', 'strapi-firebase-auth'), 11 | username: env('DATABASE_USERNAME', ''), 12 | password: env('DATABASE_PASSWORD', ''), 13 | }, 14 | options: { 15 | authenticationDatabase: env('AUTHENTICATION_DATABASE', null), 16 | ssl: env.bool('DATABASE_SSL', false), 17 | }, 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /config/functions/bootstrap.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const admin = require("firebase-admin"); 4 | const serviceAccount = require("../../serviceAccountKey.json"); // add serviceAccountKey from firebase to root directory 5 | module.exports = () => { 6 | admin.initializeApp({ 7 | credential: admin.credential.cert(serviceAccount), 8 | }); 9 | strapi.firebase = admin; 10 | }; 11 | -------------------------------------------------------------------------------- /config/functions/cron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Cron config that gives you an opportunity 5 | * to run scheduled jobs. 6 | * 7 | * The cron format consists of: 8 | * [SECOND (optional)] [MINUTE] [HOUR] [DAY OF MONTH] [MONTH OF YEAR] [DAY OF WEEK] 9 | * 10 | * See more details here: https://strapi.io/documentation/v3.x/concepts/configurations.html#cron-tasks 11 | */ 12 | 13 | module.exports = { 14 | /** 15 | * Simple example. 16 | * Every monday at 1am. 17 | */ 18 | // '0 1 * * 1': () => { 19 | // 20 | // } 21 | }; 22 | -------------------------------------------------------------------------------- /config/functions/responses/404.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (/* ctx */) => { 4 | // return ctx.notFound('My custom message 404'); 5 | }; 6 | -------------------------------------------------------------------------------- /config/server.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | host: env('HOST', '0.0.0.0'), 3 | port: env.int('PORT', 1337), 4 | admin: { 5 | auth: { 6 | secret: env('ADMIN_JWT_SECRET', '0d259a5cec88bd54ebf1f600685ccc74'), 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /extensions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambrohan/strapi-firebase-auth/3e08358ec1b20c62c07923bcfc011d46396c7ffc/extensions/.gitkeep -------------------------------------------------------------------------------- /extensions/users-permissions/config/jwt.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jwtSecret: process.env.JWT_SECRET || 'd03fb735-1d21-4bcf-9ba1-de653726fda3' 3 | }; -------------------------------------------------------------------------------- /extensions/users-permissions/config/policies/permissions.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | 3 | module.exports = async (ctx, next) => { 4 | let role; 5 | 6 | if (ctx.state.user) { 7 | // request is already authenticated in a different way 8 | return next(); 9 | } 10 | 11 | if (ctx.request && ctx.request.header && ctx.request.header.authorization) { 12 | try { 13 | const { id } = await strapi.plugins[ 14 | "users-permissions" 15 | ].services.jwt.getToken(ctx); 16 | 17 | if (id === undefined) { 18 | throw new Error("Invalid token: Token did not contain required fields"); 19 | } 20 | 21 | // fetch authenticated user 22 | ctx.state.user = await strapi.plugins[ 23 | "users-permissions" 24 | ].services.user.fetchAuthenticatedUser(id); 25 | } catch (err) { 26 | // decode firebase tokens 27 | try { 28 | const idToken = ctx.request.header.authorization.split(" ")[1]; 29 | const decodedToken = await strapi.firebase 30 | .auth() 31 | .verifyIdToken(idToken); 32 | ctx.state.user = { ...decodedToken }; 33 | if (decodedToken.strapi_uid) { 34 | ctx.state.user.id = decodedToken.strapi_uid; 35 | } 36 | return await next(); 37 | } catch (error) { 38 | return handleErrors(ctx, err, "unauthorized"); 39 | } 40 | } 41 | 42 | if (!ctx.state.user) { 43 | return handleErrors(ctx, "User Not Found", "unauthorized"); 44 | } 45 | 46 | role = ctx.state.user.role; 47 | 48 | if (role.type === "root") { 49 | return await next(); 50 | } 51 | 52 | const store = await strapi.store({ 53 | environment: "", 54 | type: "plugin", 55 | name: "users-permissions", 56 | }); 57 | 58 | if ( 59 | _.get(await store.get({ key: "advanced" }), "email_confirmation") && 60 | !ctx.state.user.confirmed 61 | ) { 62 | return handleErrors( 63 | ctx, 64 | "Your account email is not confirmed.", 65 | "unauthorized" 66 | ); 67 | } 68 | 69 | if (ctx.state.user.blocked) { 70 | return handleErrors( 71 | ctx, 72 | "Your account has been blocked by the administrator.", 73 | "unauthorized" 74 | ); 75 | } 76 | } 77 | 78 | // Retrieve `public` role. 79 | if (!role) { 80 | role = await strapi 81 | .query("role", "users-permissions") 82 | .findOne({ type: "public" }, []); 83 | } 84 | 85 | const route = ctx.request.route; 86 | const permission = await strapi 87 | .query("permission", "users-permissions") 88 | .findOne( 89 | { 90 | role: role.id, 91 | type: route.plugin || "application", 92 | controller: route.controller, 93 | action: route.action, 94 | enabled: true, 95 | }, 96 | [] 97 | ); 98 | 99 | if (!permission) { 100 | return handleErrors(ctx, undefined, "forbidden"); 101 | } 102 | 103 | // Execute the policies. 104 | if (permission.policy) { 105 | return await strapi.plugins["users-permissions"].config.policies[ 106 | permission.policy 107 | ](ctx, next); 108 | } 109 | 110 | // Execute the action. 111 | await next(); 112 | }; 113 | 114 | const handleErrors = (ctx, err = undefined, type) => { 115 | throw strapi.errors[type](err); 116 | }; 117 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambrohan/strapi-firebase-auth/3e08358ec1b20c62c07923bcfc011d46396c7ffc/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-firebase-auth", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A Strapi application", 6 | "scripts": { 7 | "develop": "strapi develop", 8 | "dev": "strapi develop", 9 | "start": "strapi start", 10 | "build": "strapi build", 11 | "strapi": "strapi" 12 | }, 13 | "devDependencies": {}, 14 | "dependencies": { 15 | "firebase-admin": "^9.0.0", 16 | "strapi": "3.1.3", 17 | "strapi-admin": "3.1.3", 18 | "strapi-connector-mongoose": "3.1.3", 19 | "strapi-plugin-content-manager": "3.1.3", 20 | "strapi-plugin-content-type-builder": "3.1.3", 21 | "strapi-plugin-email": "3.1.3", 22 | "strapi-plugin-upload": "3.1.3", 23 | "strapi-plugin-users-permissions": "3.1.3", 24 | "strapi-utils": "3.1.3" 25 | }, 26 | "author": { 27 | "name": "A Strapi developer" 28 | }, 29 | "strapi": { 30 | "uuid": "727d57fc-0fa3-431f-b01b-26ccc3cbba04" 31 | }, 32 | "engines": { 33 | "node": ">=10.0.0", 34 | "npm": ">=6.0.0" 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /public/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambrohan/strapi-firebase-auth/3e08358ec1b20c62c07923bcfc011d46396c7ffc/public/uploads/.gitkeep --------------------------------------------------------------------------------