Implementing Authentication and Authorization in Vue
- Share:
Vue is a versatile web framework that allows users to build highly scalable user interfaces for any web application. As these apps are usually used by several users, you need a way to authenticate them, ensuring they are who they claim to be, and define what actions they can perform through authorization.
This guide will walk you through implementing authentication in your Vue application with Firebase and implementing authorization through Attribute-Based Access Control (ABAC) and Relationship-Based Access Control (ReBAC) with Permit.io.
To do that, we will be using a demo food delivery application. The code for this demo application is available here.
How to Set Up Firebase in Vue
Firebase is a Google-provided backend solution that provides authentication, database, and hosting, among other services. Most importantly, it saves us the hassle of building user authentication directly into our backend from scratch, giving us a secure implementation that allows us to focus on building app logic.
The Firebase Authentication service has multiple sign-in methods, such as email and password, phone number, sign-in with Google, etc. Therefore, you can choose any or multiple methods for authentication while building. This also applies to whatever web framework you are using, Vue included.
To get started, your first step will be to set up Firebase in your code.
This requires a Google Account to access the Firebase Console and some activity in your codebase. In addition, you need a Firebase project, which you can either create in the Firebase Console or use an existing one.
Let us use an example project to demonstrate these steps in this tutorial:
For the sake of our example, we’ll use a demo food delivery application.
In this example app, a customer can place an order with a vendor. A rider can then pick up the order from the vendor and, in turn, deliver that order to the customer. The food delivery could also have an admin who oversees the system and assigns riders to orders.
We will continue using food delivery throughout this tutorial to explain how authentication and authorization work. To keep things simple, we already have a working version of the Vue codebase for food delivery which you can view here. Clone it with Git and check it out!
Also, create a Firebase project if you don’t have one:
Next up, create a web app in the Firebase console. We should do this within our new Firebase project. Creating the web app in Firebase will generate the Firebase config object to use inside Vue.
Create a src/stores/firebase.ts
file and paste the generated config there.
// In src/stores/firebase.ts
export const firebaseConfig = {
apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: 'XXXXXXXX.firebaseapp.com',
projectId: 'XXXXXXX',
storageBucket: 'XXXXXXXX.firebasestorage.app',
messagingSenderId: '000000000000',
appId: '1:000000000000:web:XXXXXXXXXXXXXXXXXXXXXX',
measurementId: 'G-XXXXXXXXXX'
};
Let’s proceed to authenticating our users!
How to Authenticate Users with Firebase in Vue
As mentioned previously, Firebase Authentication supports multiple sign-in methods. To authenticate users with Firebase, choose the sign-in method(s) you want to support for your users. In your product, you can use any one or multiple options at the same time. Using multiple sign-in methods gives users an improved experience as they can choose what they want.
For this tutorial, let’s stick to the “Sign in with Google” method. Signing in with Google makes life easy for everyone - users don’t need to enter/create passwords, and we don’t need to handle verifying email, password recovery, or other authentication flows.
To use “Sign in With Google,” go to your project's Firebase console, then to the Authentication tab, and enable the “Google” sign-in method.
After choosing and enabling sign-in methods, use Firebase’s SDK in the frontend to sign in users. This way, when users interact with sign-in buttons in the app, we call Firebase functions, and Firebase will handle the rest.
In this tutorial, we have already done this in the src/stores/auth.ts
file in the signInWithGoogle
function. As you can see, we simply used the GoogleProvider
for signing in with Firebase in the Vue code.
// In src/stores/auth.ts
// ... imports
export const useAuthStore = defineStore('auth', () => {
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// ...
const signInWithGoogle = async () => {
try {
await signInWithPopup(auth, new GoogleAuthProvider());
} catch (e) {
toastError(`${e}`);
}
};
// ...
});
In this example, we used the popup variant of signing in with Google. Firebase also provides a “signing in with redirect” option. To use “sign in with redirect”, follow Firebase’s best practices on that.
Our application is built so that every user is expected to be signed in—we want only signed-in users to be able to access our app.
To enforce this, we can add a router guard to Vue's router. This guard will redirect all website visitors to the sign-in page if they are not authenticated and prevent access to the sign-in page if there is a currently signed-in user.
// In src/router/index.ts
router.beforeEach((to, from, next) => {
const auth = useAuthStore();
if (to.name !== 'sign-in' && !auth.isSignedIn) next({ name: 'sign-in' });
else if (to.name === 'sign-in' && auth.isSignedIn) next(from);
else next();
});
This is especially important for authorization because you can’t authorize someone who is not authenticated. Authenticating users is separate from authorizing them. We first need to authenticate users to know who they are. Then, we authorize them by permitting them to carry out actions on system resources.
We’ve just looked at authentication with Firebase. Let’s now discuss authorization.
Authorization in Vue.js
Authorization is about granting users access to specific parts of our application based on who is performing an action, ensuring we enforce access based on business logic.
Just as with authentication, there are multiple ways of implementing Authorization. These are called access control models. Common examples of these include Role Based Access Control (RBAC), Attribute Based Access Control (ABAC), and Relationship-Based Access Control (ReBAC).
RBAC is usually the simplest to implement. In RBAC, you group permissions into roles (like “admin,” “editor,” “vendor,”…) and assign these roles to users. RBAC is efficient and is usually the first line of authorization before progressing into more complex ones.
A step-by-step guide on how to implement RBAC in Vue.js is available here.
The thing is, RBAC can often be lacking when it comes to more complex scenarios that require more fine-grained authorization methods. Think about an access control policy like:
A User can only access specific VIP discounts at selected restaurants if they are in a specific geo-location, and they've had a premium account for three years.
You can easily see how a role-based access control system would be impossible to use for this scenario. This is where models like ABAC and ReBAC come into play.
Why Attribute-Based Access Control (ABAC)?
Attribute-Based Access Control (ABAC) takes roles one step further, adding an additional component into the mix: Attributes.
Attributes allow us to define our users, resources, and actions more accurately, creating more granular access control policies based on these distinctions.
Common attributes can include things like like age, subscription level, account type, geo-location, etc.
In the context of our application, attributes allow us to create policies such as:
Customers who make an order whose cost is greater than 💷500 get free delivery
We will implement this policy further in this tutorial.
We can also, for example, decide that only riders who have delivered at least 500 rides can make free deliveries. Just as with orders, we’ll also be showing how this can be implemented in the coding section.
Attributes used for access control mustn’t necessarily be scoped to the resource or to the user. A given ABAC policy can use attributes from both resources and users simultaneously. We are also not restricted to checking only one attribute. A given ABAC policy can involve multiple attributes on the same resource.
However, ABAC is not the only way to define fine-grained access control policies. Another option is Relationship-Based Access Control (ReBAC).
Why Relationship-Based Access Control (ReBAC)?
ReBAC gives access based on affiliations or links, allowing us to define a policy in which a user (or an identity) must have a link with a particular resource to get access.
That’s why in ReBAC, we must think about instances of resources. Because the links are peculiar between instances and not on overall resources.
Take a bank, for example. There are many bank accounts and many account holders. Each bank account is unique, with its number and holder. While authorizing, each bank account is an instance.
ReBAC allows us to define rules based on such things as ownership or hierarchies. In a bank, some actions can only be performed by the person who owns the account.
Most of the time, ReBAC leverages relationships in your database.
Going back to the food delivery service example, we can use ReBAC to ensure that a vendor can only fulfill meals in orders if they were the creator of those meals. Fulfilling a meal here means saying that the meal is prepared and ready to be delivered.
We will implement ReBAC with this example later in this tutorial.
How to choose Authorization models (RBAC, ABAC, or ReBAC)
At the end of the day, the choice between different authorization models depends on your specific use case and the granularity of permissions you want to give. In most scenarios, you will have to combine different kinds of access controls in the same application.
Nonetheless, we recommend that you start with the basics and then expand to the most granular or fine-grained permissions.
Remember to follow the principle of least privilege. It states that we should only give people the minimum permissions they need. Just ensure that the permissions involved at each check are exactly what is needed by the business logic and nothing more.
You can read more about the choice between policy models here
That said, let us use our food delivery service example to demonstrate how to implement ABAC and ReBAC with Permit.io as our authorization provider.
How to setup Permit.io for Vue Authorization
In Permit.io, everything we do is in the context of a project. A Permit project is where you scope resources and users for your app.
If you came here from the Vue RBAC tutorial, you can continue with the same Permit project you had set up as we built up from there. Otherwise, simply sign in to your account at app.permit.io and create a workspace or project for this tutorial.
Permit primarily works in server-side code. This is because that’s where you would enforce security checks. If not, how would you prevent client-side attacks or impose business logic?
Well, we can still use Permit for Vue permissions by integrating Permit into the system’s backend code. If we don’t have one, we can easily build a small backend to enforce authorization checks.
The necessary server-side code for this example is in the backend
directory of this demo application. It uses NodeJS/Express and requires your Permit token to work.
You can obtain it from the Permit console, create a .env
file in the backend
directory, and paste in the token like so:
PERMIT_TOKEN=*paste-token-here*
The Permit token is also very useful in setting up the PDP (Policy Decision Point) for our application. It is a network node that handles authorization queries using policies and contextual data. It is like an engine that hosts the access control computation and abstracts authorization for us.
Permit provides a public cloud PDP for everyone. However, if you want to do complex Authorization like ABAC or ReBAC, you need to deploy a PDP yourself. And that’s a second place where the permit token comes in.
Locally, we can use docker to run a PDP for testing. In production, you can deploy one with your infrastructure. See it as Permit’s way of putting you in charge.
To run a local PDP (which we need for this tutorial), run the following command in a separate terminal. It continuously displays logs for you:
docker run -it \\
-p 7766:7000 \\
--env PDP_API_KEY=<your-permit-api-key> \\
--env PDP_DEBUG=True \\
permitio/pdp-v2:latest
In the backend, we initialize the Permit instance in the /backend/src/middleware/permissions/permit.ts
file. We used the token from the .env
file and the local cloud PDP for this initialization. This file exports the permit
instance so that other authorization middleware will use it directly. This effectively completes our setup of Permit for authorization
import 'dotenv/config';
import { Permit } from 'permitio';
// Construct the Permit object using the public Cloud PDP and your Permit token
export const permit = new Permit({
pdp: '<http://localhost:7766>',
token: process.env.PERMIT_TOKEN
});
Up next, how does the backend know which user is executing an action in Vue? How can the backend tell which user signed into Vue through Firebase? The solution is to use Express middleware to verify the signed-in Firebase user.
How to Verify Firebase Authentication Users in the Middleware
Firebase's idToken
feature makes it easy for custom backends to identify the currently signed-in user. In the front end, we can generate the idToken
from the Firebase user object and send it along in the HTTP header when making calls to the backend.
That’s what’s happening in the src/stores/api.ts
file:
// In src/stores/api.ts
// ...
// when preparing headers for HTTP calls, obtain the idToken
// of the currently signed-in Firebase user and send across with
// the call. If there is no user, don't send any token.
...(auth.currentUser
? { Authorization: `Bearer ${await auth.currentUser.getIdToken()}` }
: {})
On the server side, we can use the Firebase Admin SDK to determine the user's user ID by verifying the idToken. The backend/src/middleware/authentication.ts
file contains middleware for verifying the authenticated user.
If the user is verified, we add the uid
(user ID) from the decoded result to res.locals
. Express uses this object (res.locals) to share data across middleware.
// In backend/src/middleware/authentication.ts
import 'dotenv/config';
import { NextFunction, Request, Response } from 'express';
import { applicationDefault, initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
// Initialize Firebase Admin SDK
initializeApp({ credential: applicationDefault() });
// Express Middleware to verify the Firebase Auth idToken
export const verifyIdToken = async (
req: Request,
res: Response,
next: NextFunction
) => {
// Extract the token from the request headers
const token = (req.headers.authorization ?? '').split('Bearer ')[1];
try {
// Verify the token
const decoded = await getAuth().verifyIdToken(token);
// Attach the UID to the request object
res.locals.uid = decoded.uid;
// Continue to the next middleware
next();
} catch (error) {
// Return an Unauthenticated response if the token is invalid
res.status(401).send({ success: false, message: 'You are not signed in.' });
}
};
However, for the above to work, we also need to connect the backend to our Firebase project. To do this, we use a service account JSON file. Firebase gives you this in the Firebase console. Obtain it here (in the service accounts tab of your project settings).
Then, rename it to firebase-service-account.json
and put it in the backend folder.
We also need to tell Firebase where the file is. To do that, append the following to the .env
file after the Permit token line.
GOOGLE_APPLICATION_CREDENTIALS=firebase-service-account.json
The first import of the authentication middleware is import 'dotenv/config';
.
This line loads the .env
file in the middleware so that the applicationDefault()
call by Firebase can see the service account JSON file that we downloaded.
Now that we know the currently signed-in user in the frontend, how do we effectuate access control checks in the backend? How do we use Permit for authorization?
The solution is the permit.check()
function. Permit provides it in every SDK to tell whether the acting user is allowed to take an action. The permit.check
function takes details of the involved user or identity, the action, and details of the resource. We will use this in the following ABAC and ReBAC sessions. Each one needs some configurations in the Permit console UI to fully implement them.
How to Implement Attribute-Based Access Control (ABAC) in Vue
ABAC can use one or more attributes on resources and or users. A given check that includes ABAC can involve attributes in either the user, the target resource, or both. Either way, we have to specify the attributes and afterwards define a set for our policy.
Permit allows us to add attributes to users (or identities) and resources. How we do it all depends on our business logic. It is mainly to figure out which ABAC policies we want to administer, deduce the attributes, and add them in Permit.
For example, for Orders with a cost greater than 500:
- We first need to specify “cost” as an attribute on the Order with the type number.
- After that, we create a Resource Set for these Orders and define the cost attribute to be greater than or equal to 500 while creating.
The same applies to users:
- We can add “number_of_rides” as an attribute
- Then, we create a User Set that we will use for ABAC.
Head to the Policy Editor at app.permit.io and add the attributes to the Order resource and users. If you don’t have the Order resource, create it. Also, add a create-with-free-delivery
action for the Order resource.
Add users’ attributes here. Then, create the appropriate resource set and user set, as we mentioned above under the ABAC Rules tab.
Lastly, to set up ABAC in the Permit UI, we need to define the ABAC policy. After creating the user and resource sets above, the Policy Editor in Permit will auto-include them. Creating the ABAC policy is as easy as toggling permissions for the resource and user sets for the actions we want (create-with-free-delivery
and deliver
):
The above steps are a one-time thing. You don’t have to redo them again. Creating attributes and ABAC sets helps in properly defining what’s in for authorization. This is a priceless utility that Permit helps you with. It is much better than building permissions everywhere in codebases. The only time you might deal with the sets is when you want to edit policies.
Given that ABAC rules are set, how do we enforce them in the check function? How do we make permit.check
to account for attributes and not only roles?
The answer is in providing those attributes in the backend while calling permit.check
. When doing only Role-Based Access Control, you can call the check function in the following way:
permit.check(user, action, resource);
Now that we are adding ABAC, we can expand the user to be an object with the user’s ID and attributes. We can also do the same for the resource.
permit.check(
{ "key": "userId", "attributes": { "attrb1": 4 } }, // user
"send", // action
{ "type": "orderType", "attributes": { "attrb1": 35 } }, // resource
);
We are doing this in the /backend/src/middleware/permissions/issue-free-delivery.ts
.
It is a middleware for order creation (to allow FREE delivery). The code works to show an example, you will have a more robust implementation in production or real-life work.
// In backend/src/middleware/permissions/issue-free-delivery.ts
import { NextFunction, Request, Response } from 'express';
import { permit } from './permit';
// Express Middleware to check if order can get free delivery
export const issueFreeDelivery = async (
_: Request,
res: Response,
next: NextFunction
) => {
// Obtain userId from res.locals. Has been set by verifyIdToken middleware
const userId = res.locals.uid as string;
// Obtain order cost from res.locals.
const { totalPrice } = res.locals.newOrder;
// Check with Permit if order can get free delivery
const canHaveFreeDelivery = await permit.check(
userId,
'create-with-free-delivery',
{ type: 'Order', attributes: { cost: totalPrice } }
);
// Issue free delivery if authorised
if (canHaveFreeDelivery) res.locals.newOrder.deliveryFee = 0;
// Continue to next handler
next();
// Not handling when the free delivery is not issued inorder not to
// break the flow of the application as users can still proceed to pay
// for delivery
};
That was an ABAC application for allowing free delivery for customers when placing orders. We can have something similar for riders when making deliveries: authorizing riders to execute free deliveries based on their number of rides. The following is how it is in the /backend/src/middleware/perrmissions/check-rider-eligibility.ts
file:
// In backend/src/middleware/permissions/check-rider-eligibility.ts
import { NextFunction, Request, Response } from 'express';
import { permit } from './permit';
// Express Middleware to check if a rider can delivery a free delivery order
export const checkRiderEligibility = async (
_: Request,
res: Response,
next: NextFunction
) => {
// Obtain userId from res.locals. Has been set by verifyIdToken middleware
const userId = res.locals.uid as string;
// Obtain order cost from res.locals.
const { orders, orderIndex } = res.locals;
const { totalPrice } = orders[orderIndex];
// Check with Permit if the rider can make the delivery
const canRiderDeliver = await permit.check(
{ key: userId, attributes: { number_of_rides: 505 } }, // hardocded 505 for demo
'deliver',
{ type: 'Order', attributes: { cost: totalPrice } }
);
// Permit the rider to deliver the order if authorised
if (canRiderDeliver) {
next();
} else {
res.status(401).json({
success: false,
message: 'Rider not authorised for free delivery'
});
}
};
Though we hardcoded the number of rides for demonstration, the enforcement works. If you give a lower number of rides for an order cost above 500, authorization will fail. Also, even though the example code uses only one attribute, you can supply as many as your business logic demands.
Additionally, you can combine ABAC with roles.
The Eligible Rider check relies only on the number of rides (the ABAC User Set). We could combine this ABAC check with the Role-based check of the rider role. For a user to be eligible, they need to have the top-level rider role and also the required number of rides. This can be enforced in the policy table (though we didn’t do that here).
To see the ABAC authorization in action, copy the user ID of your signed-in user from the Firebase console and use it to create a user in Permit. Assign it the customer role (create it if you don’t have it).
In the food delivery UI of this tutorial, create an order with so many foods and see the free delivery applied. Navigate to the Rider tab and deliver the order, and it will pass. If you edit the hardcoded number_of_rides
in the middleware to anything below 500, delivery won’t be allowed. If you check the terminal where you launched the local PDP, you will see the audit log for the ABAC policy.
How to Implement Relationship-Based Access Control (ReBAC) in Vue
To integrate Relationship-based access control, we have to create resource instances in the Permit UI. We also have to create instance roles on those resources. An instance is represented by its ID or key. It is a replica of a resource.
In contrast to regular roles, an instance role is represented as “Resource#Role” like Order#Vendor. It is assignable to users in the context of a particular instance.
In production, when doing ReBAC, you will have many instances for each resource. They will be created programmatically by syncing with Permit API and SDKs. On the other hand, the instance roles won’t be so numerous. You only need them as business logic defines.
Instance roles can only gain authorization on the target instance they are attached to. They don’t act in all instances.
That’s the major difference between role-based and relationship-based access controls: There must be an affiliation in ReBAC which is enforced with instances and instance roles.
Furthermore, ReBAC allows for role derivations. If a resource is a parent/owner to another resource, we can derive permissions to this child resource. In other words, role authorization on a parent will also work on a child resource if we set that in the policy table.
Let’s create a ReBAC policy in the Permit UI with the Food Delivery example. We will showcase the authorization of vendors fulfilling orders through ReBAC. Fulfilling here means that the vendor has prepared the order, and the order is ready for delivery. The steps are as follows:
- Create Vendor instance role on Order
- Edit the Policy Table to allow “fulfill” action on Orders only by relating Vendor instance role holders.
The above steps create instances of the resources in Permit and assign the instance roles (of each instance) to given users. The permit.check
function will only allow access for the given action if the instance roles match the instances, allowing for granular, deeply specific permissions.
That said, how do we tell the check function what resource instance to look up during authorization? How does the permit.check
function know the specific instance to allow or deny access to? The solution is to provide the key
to the resource. This key is like the instance identifier for that resource. We can provide a key just as we provided attributes.
That’s what we have in the /backend/src/middleware/permissions/check-fulfilling-vendor.ts
middleware file.
We are using this middleware to carry out ReBAC for fulfilling orders. It is part of the handlers for the “fulfill order” route in Express. Notice how we use the order ID as the key for the order resource in the check function:
// In backend/src/middleware/permissions/check-fulfilling-vendor.ts
import { NextFunction, Request, Response } from 'express';
import { permit } from './permit';
// Express Middleware to check if a vendor owns meals in an order
export const checkFulfillingVendor = async (
_: Request,
res: Response,
next: NextFunction
) => {
// Obtain userId from res.locals. Has been set by verifyIdToken middleware
const userId = res.locals.uid as string;
// Obtain order ID from res.locals.
const { orders, orderIndex } = res.locals;
const { id: orderId } = orders[orderIndex];
// Check with Permit if the vendor can fulfill the order
const canFulfillOrder = await permit.check(
{ key: userId },
'fulfill',
{ type: 'Order', key: orderId } // providing orderId for ReBAC
);
// Permit the rider to deliver the order if authorised
if (canFulfillOrder) {
next();
} else {
res.status(401).json({
success: false,
message: 'Vendor not authorised to fulfill order'
});
}
};
To see the above in action:
- Create an Order in the example Food Delivery.
- Take note of its Order ID.
- Create an Order instance in Permit with that ID (Directory > Instances).
- Grant the instance role on the Firebase user ID in the Permit UI with that Order ID.
- Then, go back to the Food Delivery and fulfill that order as a vendor, and ReBAC will allow access.
- You can test denies similarly, too.
You won’t need to manually add every resource instance in the Permit UI as we did in this tutorial. We aim to understand how the permissions work. Permit provides an easy yet robust API and SDK for syncing instances and relationships. Learn more about that here.
The example in this tutorial doesn’t showcase role derivation but it is something we should also discuss. Resources normally relate to each other. For example, in the food delivery, an Order consists of Meals. This means that we can create a relationship between Order and Meal.
Additionally, we can also create a Vendor instance role on Meal. Then in the policy table, we can create a role derivation for the vendor instance roles on Orders to derive the permissions of Vendor instance roles on meals.
This will make the holders of Order#Vendor capable of executing Meal#Vendor actions on related Meal instances.
The Permit UI makes derivations easy, so you can apply them as your business logic demands.
Aside from deriving permissions, we can also combine ABAC and ReBAC at the same time. For example, in addition to Vendors preparing only orders with their meals, we could add ABAC conditions like vendors being verified or paying some subscription before they could prepare meals. Authorization is dynamic. Just build as you need. And that sums up the conversation on ReBAC.
Summary
In this tutorial, we explored how to implement authentication and authorization in a Vue application. We started by setting up Firebase Authentication to verify users, ensuring that only authenticated users can access our app. From there, we dove into authorization models, implementing Attribute-Based Access Control (ABAC) and Relationship-Based Access Control (ReBAC) using Permit.io.
Through ABAC, we defined fine-grained access policies based on attributes like order cost and ride history, allowing us to enforce conditional access control dynamically. With ReBAC, we restricted access based on relationships, ensuring that only the correct vendors could fulfill their own orders.
With Permit.io, we abstracted complex access control logic, making it easier to enforce security while focusing on core application features. As your application grows, you can combine RBAC, ABAC, and ReBAC to create a scalable, secure, and flexible authorization system.
Got questions? Need help? Join our Slack community!
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker