Firebase + Nuxt, Role Based Authentication & Authorization
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.
- 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 statusexports.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 🍎