In the part 1 of User Permissions and the Principle of Least Privilege on API Endpoints using Firebase, we explained the need for managing user authentication and authorization using roles and permissions.
We also applied the principle to various server endpoints. Here, we are jumping straight into client implementation using Vue.js, An approachable, performant and versatile framework for building web user interfaces.
A quick display of the project tree is depicted below:
.
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── vite.svg
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── avatar.png
│ │ └── vue.svg
│ ├── components
│ │ ├── core
│ │ │ └── Header.vue
│ │ ├── generics
│ │ └── user
│ │ ├── User.vue
│ │ ├── UserCard copy.vue
│ │ ├── UserCard.vue
│ │ ├── UserCreate.vue
│ │ ├── UserSkeleton.vue
│ │ ├── UserUpdate.vue
│ │ └── Users.vue
│ ├── config
│ │ └── index.js
│ ├── database
│ │ └── index.js
│ ├── firebase
│ │ └── index.js
│ ├── main.js
│ ├── router
│ │ └── index.js
│ ├── service
│ │ └── index.js
│ ├── store
│ │ ├── index.js
│ │ └── modules
│ │ └── user.js
│ ├── style.css
│ ├── utils
│ │ └── index.js
│ └── views
│ ├── Dashboard.vue
│ ├── Login.vue
│ ├── NotFound.vue
│ └── Register.vue
└── vite.config.js
Unlike in the server side, the client side is straight forward as most of the heavy lifted has already been performed on the server. However, few directories deserve a mention: components
, config
, firebase
, store
, and utils
.
The component directory has all the various pages for performing CRUD
operations on the client side. The config directory has axios
implementation for making requests and getting responses from the server.
export const request = async (url, method, token, payload, query) => {
return await axios({
method: method,
url: `${url}`,
data: payload,
params: query,
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${store.getters.idToken}`,
},
json: true,
});
};
Then, the firebase directory has the Firebase configuration object
containing keys and identifiers for our application.
The store directory has, well, the Veux
store that manages our data state and help centralize the implementation of requests to API endpoints. We will come back to Veux store shortly.
And finally, the utils directory that has our custom defined functions. Within this file, we defined the isAuhorised
method that together with firebase onAuthStateChanged
supports the re-validation of user token
and claims
.
/**
* check user authorisation
* @param {string} action
* @returns {boolean}
*/
export const isAuhorised = (action) => {
// TODO: get current user permissions
const { permissions } = store.getters.profile;
// TODO: verify if user has permission for the intended action
return permissions.some(permission => permission.name === action && permission.value === true);
};
The main.js file serves as the entry point to our client application. Here, we use the onAuthStateChanged
method from the firebase/auth
to identify and setup the current signed in user, authentication status, role and permissions custom claims, and id token
.
onAuthStateChanged(auth, async user => {
if (user && user.emailVerified) {
const idTokenResult = await user.getIdTokenResult();
const { claims: { entity, name, permissions } } = await user.getIdTokenResult();
if (idTokenResult && idTokenResult.claims) {
await store.dispatch('setProfile', { ...user, entity, name, permissions });
await store.dispatch('setClaims', idTokenResult.claims);
await store.dispatch('setIsAuthenticated', true);
await store.dispatch('setIdToken', idTokenResult.token);
await store.dispatch('setCurrentUser', idTokenResult.claims.entity);
}
}
});
Finally, within the Veux store, we will use user.js
as the entry point for invoking the user endpoints. To be sure that a user is granted only the necessary access needed to execute a task, we are going to use the isAuhorised
function. The necessary action is passed as a parameter which either allow or deny requests to the server.
if(!isAuhorised(action)) return;
So, when a user tries to make an API call to the getUsers
endpoint, we can check if the user has enough privilege to continue:
async getUsers(context, payload) {
try {
// TODO: check user permission
if(!isAuhorised('readAll')) return;
// TODO: api call
await context.dispatch('setLoading', true);
if (!payload && context.state.users && !!context.state.users.length) {
await context.dispatch('setLoading', false);
return context.state.users;
}
const { data } = await userApi.getUsers(context.rootGetters.idToken);
if (!Array.isArray(data)) return;
await context.dispatch('setLoading', false);
context.commit('SET_USERS', data);
return data;
} catch (error) {
console.log('Error: ', error);
return;
}
},
This way, we are making sure that users do not attempt a request unless they have the permission to do so. The repository for this tutorial is on GitHub.
If you like the article, do like and share with friends.
Reference:
https://www.cyberark.com/what-is/least-privilege/
Top comments (0)