Firebase + Nuxt, Role Based Authentication & Authorization

Alex Kasongo
JavaScript in Plain English
10 min readOct 15, 2020

--

In this article, you’re going to learn how to set user roles using Auth Custom Claims and store user data to the Cloud Firestore when a new Firebase user account is created.

In addition, I will be showing you how to guard routes based on user role when signing in.

Finally, I will show you how to get all the user accounts when the signed-in user has admin privileges and I will show you how to change user roles using callable Cloud Functions.

These are the 3 user roles we’ll be creating:

  • The Admin role will have access to all the users stored on the database and will be given permission to change user roles using Security Rules.
  • The Customer role will have access to Customer View and it will be set as the default role as most of the users will be under this role.
  • The Subscriber role will have access to Subscriber View.
  1. Up and running Nuxt Project

I have created a starter project using npx create-nuxt-app <name>and created six pages under src/pages folder. Nuxt handles all routing. You can clone this repo and code along. You can also add the code to an existing project depending on your particular use case.

If you’re looking to implement this in a Vue.js application, feel free to clone the Vue version of the code here

2. Install firebase and Create a Firebase User Account

npm install firebase

Register firebase.js in plugins array inside nuxt.config.js

// nuxt.config.js
Plugins = [
'~/plugins/firebase.js',
],

Go ahead and create a project on the Firebase Console. In your Nuxt app create a file inside src/plugins called firebase.js and inside this file include your firebase initialization code.

// firebase.js
import "firebase/auth";
import "firebase/firestore";
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "**************",
authDomain: "**************",
databaseURL: "**************",
projectId: "**************",
storageBucket: "",
messagingSenderId: "**************",
appId: "**************",
measurementId: "**************"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

3. Update Default.vue

// default.vue
<template>
<div>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Navbar</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<nuxt-link class="nav-link" to="/login">Login</nuxt-link>
</li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/register-customer"
>Register Customer</nuxt-link
>
</li>
<li class="nav-item">
<nuxt-link class="nav-link" to="/register-admin"
>Register Admin</nuxt-link
>
</li>
</ul>
</div>
</nav>
<div class="container mt-5">
<Nuxt />
</div>
</div>
</template>

<style>
html {
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}

*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}

.button--green {
display: inline-block;
border-radius: 4px;
border: 1px solid #3b8070;
color: #3b8070;
text-decoration: none;
padding: 10px 30px;
}

.button--green:hover {
color: #fff;
background-color: #3b8070;
}

.button--grey {
display: inline-block;
border-radius: 4px;
border: 1px solid #35495e;
color: #35495e;
text-decoration: none;
padding: 10px 30px;
margin-left: 15px;
}

.button--grey:hover {
color: #fff;
background-color: #35495e;
}
</style>

3. Register Customer

Let’s create a registration form. I use Bootstrap CDN but feel free to use your favorite. User data will be stored in the Cloud Firestore under the roles collection. This way, you can get all the user information later when having admin privileges.

src/pages/register-customer

// src/pages/register-customer<template>
<div class="row">
<div class="col-sm-4 off-set">
<form @submit.prevent="onSubmit">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input
type="email"
class="form-control"
placeholder="Enter email"
v-model="email"
/>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input
type="password"
class="form-control"
placeholder="Password"
v-model="password"
/>
</div>
<button type="submit" class="btn btn-primary">Register Customer</button>
</form>
</div>
</div>
</template>

<script>
import * as firebase from "firebase/app";
import "firebase/auth";

export default {
name: "RegisterCustomer",
data: () => ({
email: "",
password: "",
}),
methods: {
async onSubmit() {
try {
var { user } = await firebase
.auth()
.createUserWithEmailAndPassword(this.email, this.password);
// signout
firebase
.auth()
.signOut()
.then((user) => {
this.$router.push("/login");
});
} catch (error) {
console.log("🤡", error.message);
}
},
},
};
</script>

When the user role is set via auth custom claims, it will become available after signing out and signing back in.

For that reason, I have the signout functionality below after creating a new user account.

4. Add Firebase Cloud Functions

4.1 Important: Node.js versions 8, 10, and 12 are supported.

npm install -g firebase-tools

Or

sudo npm install -g firebase-tools

4.2 Important: update both the Firebase CLI and the SDK

npm install firebase-functions@latest firebase-admin@latest --save
npm install -g firebase-tools

4.2 Initialize your project

Run firebase login to log in via the browser and authenticate the firebase tool.

Run firebase init

For our case, let’s pick the following:

Which Firebase CLI features do you want to set up for this folder? Functions

What language would you like to use to write Cloud Functions? JavaScript

Do you want to use ESLint to catch probable bugs and enforce style? Yes

Do you want to install dependencies with npm now? Yes

That’s all!

Let the Firebase CLI do the project scaffolding, and get the project files ready.

src/functions

5. Add Admin Auth Custom Claims

As you know it’s not a good idea to set the admin role from the client, so I will be using Firebase Cloud Functions to add that.

AddUserRole() function will be triggered when a new Firebase user account is created.

functions/index.js

// functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin')
admin.initializeApp()const db = admin.firestore()// trigger function on new user creation.
// when a new user is created this fucntion is triggered. When triggered a defualt
// data object is pushed to the roles collection, this object contains the user's role status
exports.AddUserRole = functions.auth.user().onCreate(async (authUser) => {
if (authUser.email) {
const customClaims = {
admin: true,
};
try {
var _ = await admin.auth().setCustomUserClaims(authUser.uid, customClaims)
return db.collection("roles").doc(authUser.uid).set({
email: authUser.email,
role: customClaims
})
} catch (error) {
console.log(error)
}
}
});

Important: Deploy function to Firebase.

firebase deploy --only functions

6. Register Admin

It is not good practice to set the admin role from the client, so I will be using Firebase Cloud Functions to do that.

src/pages/register-admin.vue

// src/pages/register-admin<template>
<div class="row">
<div class="col-sm-4 off-set">
<form @submit.prevent="onSubmit">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input
type="text"
placeholder="Email"
v-model="email"
class="form-control"
required
/>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input
type="password"
placeholder="Password"
v-model="password"
class="form-control"
required
/>
</div>
<button type="submit" class="btn btn-primary">Register Admin</button>
</form>
</div>
</div>
</template>
<script>
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/functions";
export default {
name: "RegisterAdmin",
data() {
return {
name: "",
phone: "",
email: "",
password: "",
user: null,
};
},
methods: {
async onSubmit() {
let admin = {
role: {
admin: true,
},
};
await firebase
.auth()
.createUserWithEmailAndPassword(this.email, this.password)
.then((response) => {
if (response) {
const setAdmin = firebase.functions().httpsCallable("setAdmin");
const data = { uid: response.user.uid, role: admin.role };
setAdmin(data)
.then((result) => {
console.log(`index.js - 183 - "🎉"`, result);
})
.then(() => {
// signout
firebase
.auth()
.signOut()
.then(() => {
this.$router.push("/login");
});
});
}
})
.catch((error) => {
// Handle Errors here.
console.log("🤡", error.message);
});
},
},
};
</script>

6.1 function/index.js

Add cloud function that is called when a new admin is created. This function receives a uid and new role data.

// create admin user on signup
exports.setAdmin = functions.https.onCall(async (data, context) => {

// if (!context.auth.token.admin) return
if (!context.auth.token) return

try {
var _ = await admin.auth().setCustomUserClaims(data.uid, data.role)

return db.collection("roles").doc(data.uid).update({
role: data.role
})

} catch (error) {
console.log('🤡', error)
}

});

Important: Deploy function to Firebase.

firebase deploy --only functions

7. Login

Let’s log in to the admin account using the admin account created on the register-admin page

src/pages/login.vue

<template>
<div class="row">
<div class="col-sm-4 off-set">
<form @submit.prevent="onSubmit">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input
type="email"
class="form-control"
placeholder="Enter email"
v-model="email"
/>
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input
type="password"
class="form-control"
placeholder="Password"
v-model="password"
/>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
</template>

<script>
import * as firebase from "firebase/app";
import "firebase/auth";

export default {
name: "Login",
data: () => ({
email: "",
password: "",
}),
created() {
firebase.auth().onAuthStateChanged((userAuth) => {
if (userAuth) {
firebase
.auth()
.currentUser.getIdTokenResult()
.then((tokenResult) => {
console.log("🍎 ", tokenResult.claims);
});
}
});
},

methods: {
async onSubmit() {
try {
const { user } = await firebase
.auth()
.signInWithEmailAndPassword(this.email, this.password);
} catch (error) {
console.log("🤡", error);
}
},
},
};
</script>

8.1 Register Customer

Go to the Register Customer view and register a new customer user. Upon successful registration, a new collection called ‘roles’ will be created in the Cloud Firestore database.

8.2 Login Customer

When you log in with the customer you have already created, you should see that the customer property is set to true inside the custom claims object in your console

8.2 Register Admin

Go to the Register Admin view and register a new admin user. Upon successful registration, a new collection called ‘roles’ will be created in the Cloud Firestore database.

8.2 Login Admin

When you log in with the admin you have already created, you should see that the customer property is set to true inside the custom claims object in your console

9. Customer View

This view has a simple heading, user email and sign out button and the driver view is identical except for the heading text.

// src/pages/customer
<template>
<section>
<div class="ui middle aligned center aligned grid">
<div class="column">
<h1>Customer</h1>
<p v-if="user">Customer: {{ user.email }}</p>
<button class="btn btn-primary" @click="signout">Signout</button>
</div>
</div>
</section>
</template>
<script>
import * as firebase from "firebase/app";
import "firebase/auth";
export default {
data() {
return {
user: null,
};
},
created() {
var self = this;
firebase.auth().onAuthStateChanged(function (user) {
self.user = user;
});
},
methods: {
signout() {
firebase
.auth()
.signOut()
.then((user) => {
this.$router.push("/login");
});
},
},
};
</script>

You may wonder, what if I don’t want customers or subscribers to see the dashboard? And what if I want to change user roles such as a customer to a subscriber and vice versa?

No worries, I will cover that just in a moment.

9. Auth Guard for Authorization

Let’s create a middleware folder in src, and inside it create a file called aunthenticated.js

Important: Register middleware in nuxt.config.js

// nuxt.config.js
router: {
middleware: ['authenticated']
},

Inside the beforeEach method, I am checking to see if a user is logged in or not using the onAuthStateChanged method from Firebase.

If there is a user, get an idTokeResult that has the claims object in which I can get the user role that was set when creating a new user account.

src/middleware/authenticated.js

import * as firebase from 'firebase/app';
import 'firebase/auth';

export default function ({ app, store, route, redirect }) {

app.router.beforeEach((to, from, next) => {

firebase.auth().onAuthStateChanged(userAuth => {

if (userAuth) {
firebase.auth().currentUser.getIdTokenResult()
.then(function ({
claims
}) {

if (claims.customer) {
if (to.path !== '/customer')
return next({
path: '/customer',
})
} else if (claims.admin) {
if (to.path !== '/admin')
return next({
path: '/admin',
})
} else if (claims.subscriber) {
if (to.path !== '/subscriber')
return next({
path: '/subscriber',
})
}

})
} else {
if (to.matched.some(record => record.meta.auth)) {
next({
path: '/login',
query: {
redirect: to.fullPath
}
})
} else {
next()
}
}

})

next()

})

}

11. Change User Roles

Let’s get all the user accounts when the admin account is logged in so that you can change roles to any user from the front end.

Admin.vue

<template>
<section>
<div class="ui middle aligned center aligned grid">
<div class="column">
<h1>Admin</h1>
<p v-if="user">User: {{ user.email }}</p>
<button class="btn btn-primary mb-4" @click="signout">Signout</button>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Email</th>
<th scope="col">Role</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td data-label="Name">{{ user.email }}</td>
<td>
<select @change="changeRole(user.id, $event)">
<option :selected="user.role.subscriber" value="subscriber">
Subscriber
</option>
<option :selected="user.role.customer" value="customer">
Customer
</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>
<script>
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/functions";

export default {
name: "Admin",
data() {
return {
users: [],
user: null,
};
},
created() {
var self = this;
firebase.auth().onAuthStateChanged((user) => {
self.user = user;
});
this.users = [];
firebase
.firestore()
.collection("roles")
.get()
.then((snap) => {
snap.forEach((doc) => {
var user = doc.data();
user.id = doc.id;
console.log("🌿", doc.data());
if (!user.role.admin) this.users.push(user);
});
});
},
methods: {
signout() {
firebase
.auth()
.signOut()
.then((user) => {
this.$router.push("/login");
});
},
changeRole(uid, event) {
var setUserRole = firebase.functions().httpsCallable("setUserRole");
var data = { uid: uid, role: { [event.target.value]: true } };
setUserRole(data)
.then((result) => {
console.log("🎉", result);
})
.catch((error) => {
console.log("🤡", error);
});
},
},
};
</script>

Let’s add security rules to the database level. Only admin will be able to get to see all user data from the roles collection inside created() method.

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /roles/{uid} {
allow read, write: if request.auth.token.admin == true
}

}
}

Now, add a drop-down menu next to each user with the roles they can switch to and send a uid and new role data to the callable function called setUserRole() on the backend.

// this function can only be triggered by the admin. This function allows the admin to
// set user roles accordingly.
exports.setUserRole = functions.https.onCall(async (data, context) => {
if (!context.auth.token.admin) return
try {
var _ = await admin.auth().setCustomUserClaims(data.uid, data.role)
return db.collection("roles").doc(data.uid).update({
role: data.role
})
} catch (error) {
console.log(error)
}
});

Important: Deploy function to Firebase.

firebase deploy --only functions

First, check to see if the request was made by an admin using custom claims on the server-side. Then, set a new role to the custom claims object using the uid as well as update the corresponding document to reflect the role change.

If you have better ways to solve this, please let me know in the comment section below.

🍎 GITHUB REPO 🍎

--

--

Hi, my name is Alex. I’m a front-end engineer, passionate for the web, responsive design, & typography.