Setup Firebase (Web)
Create a Firebase project: https://console.firebase.google.com
Register your app with Firebase: Firebase Console -> Project Overview -> + Add app -> Web
Enable Firebase Console -> Authentication -> Sign-in Method
: Email / Password
NOTE: You can pre-create users at Users
tab and click on Add User
.
Add Firebase SDK: https://firebase.google.com/docs/web/setup#add-sdks-initialize. You can do so via Firebase Hosting, CDN, or module bundlers/Node.js.
Vue.js
Since I am using vue-cli
, I will use module bundlers.
Go to Vue.js project directory.
npm install --save firebasenpm install --save firebaseui
Edit main.js
import * as firebase from 'firebase/app'// import 'firebase/analytics';import 'firebase/auth'import * as firebaseui from 'firebaseui'import 'firebaseui/dist/firebaseui.css'const firebaseConfig = { apiKey: "AIza...", authDomain: "....firebaseapp.com", databaseURL: "https://....firebaseio.com", projectId: "...", storageBucket: "....appspot.com", messagingSenderId: "1020...", appId: "1:1020..."};firebase.initializeApp(firebaseConfig)
NOTE: Get firebaseConfig
from Firebase Console -> Project Overview (Click on Gear Icon) -> Project Settings -> General -> Your apps -> Select your Web apps -> Firebase SDK snippet -> Config
.
Edit main.js
after firebase.initializeApp(firebaseConfig)
.
const app = { name: 'My App', version: '1.0.1', // isAuthenticated: firebase.auth().currentUser !== null isAuthenticated: false}Vue.prototype.$app = applet initUi = falsefunction renderUi() { if (!initUi) { new Vue({ router, render: h => h(App) }).$mount('#app') initUi = true }}firebase.auth().onAuthStateChanged(user => { // user is not enough, as we need custom claims as well // app.isAuthenticated = user !== null if (user) { user.getIdTokenResult() .then((idTokenResult) => { app.isAuthenticated = idTokenResult.claims.authorized renderUi() }) } else { app.isAuthenticated = false renderUi() } // onAuthStateChanged is slightly delayed, // thus we wait for authentication before render to make sure router navigation guard works // renderUi})
Vue Router
I am using vue-router, so I am using Navigation Guards for authentication.
Edit router/index.js
- If not authorized, redirect to
/signin
.
import Vue from 'vue'import VueRouter from 'vue-router'import Home from '../views/Home.vue'import Page from '../views/Page.vue'import SignIn from '../views/SignIn.vue'Vue.use(VueRouter)const routes = [ { path: '/', name: 'home', component: Home }, { path: '/page/:id', component: Page }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') }, { path: '/signin', name: 'signin', component: SignIn },]const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, scrollBehavior (to, from, savedPosition) { if (savedPosition) { return savedPosition } else { return { x: 0, y: 0 } } }})import * as firebase from 'firebase/app'router.beforeEach((to, from, next) => { if (to.fullPath === '/about') { next() return } const isAuthenticated = Vue.prototype.$app.isAuthenticated if (!isAuthenticated && to.fullPath !== '/signin') next('/signin') else next()})export default router
SignIn Component
Edit views/SignIn.vue
<template> <div> <div class="alert alert-danger" role="alert" v-if="error"> {{ error }} <button type="button" class="btn btn-primary" @click="signIn">Try again</button> </div> <div id="firebaseui-auth-container"></div> </div></template><script>import * as firebase from 'firebase/app'import * as firebaseui from 'firebaseui'export default { name: 'signin', data() { return { error: null, from: null, uiConfig: { callbacks: { // @ts-ignore signInSuccessWithAuthResult: (authResult, redirectUrl) => { // this.$app.isAuthenticated = true // this.$router.replace(this.from || '/') const user = authResult.user this.handleSignIn(user) return false // redirect if true }, uiShown() { // login from is shown } }, // signInFlow: 'popup', // signInSuccessUrl: '/', signInOptions: [ { provider: firebase.auth.EmailAuthProvider.PROVIDER_ID, requireDisplayName: false } ] } } }, methods: { signIn() { this.$ui.start('#firebaseui-auth-container', this.uiConfig) }, handleSignIn(user) { user.getIdTokenResult(true) .then((idTokenResult) => { this.$app.isAuthenticated = idTokenResult.claims.authorized if (!this.$app.isAuthenticated) { // custom claims not updated/received yet // refresh again console.log('refresh again') this.handleSignIn(user) } else { this.$router.replace(this.from || '/') } }) .catch((error) => { if (error.code == 'auth/user-token-expired') { this.error = `${user.email} is not authorized.` this.$app.isAuthenticated = false // this.signIn() } }) } }, mounted() { this.$ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(firebase.auth()) this.signIn() }, beforeRouteEnter(to, from, next) { next(vm => { vm.from = from }) }}</script>
Cloud Functions
We need a cloud functions to which listen to user account creation and whitelist the emails
- set custom claims if email is valid
- delete account if email is invalid
const functions = require('firebase-functions')const admin = require('firebase-admin')admin.initializeApp()// TODO: retrieve authorizeEmails from firestore insteadconst authorizeEmails = [ '[email protected]', '[email protected]'];exports.authorizeUserByEmail = functions.auth.user().onCreate(async (user) => { const uid = user.uid const email = user.email if (authorizeEmails.includes(email)) { await admin.auth().setCustomUserClaims(uid, {authorized: true}) console.log(`setCustomUserClaims: ${email}`) return true } else { await admin.auth().deleteUser(uid) console.log(`Delete user: ${email}`); return false }});
NOTE: Learn about write and deploy cloud functions.
More
- The obvious loophole is anyone who knows the whitelisted email can gain access. You probably want to implement Email link authentication or perform email verification.
- You might also want to secure your cloud functions or firestore with custom claims token
References: