DEV Community

Callis Ezenwaka
Callis Ezenwaka

Posted on • Edited on

Use Firebase Auth to Manage User Permissions and Enforce Principle of Least Privilege on API Endpoints. Part 2.

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
Enter fullscreen mode Exit fullscreen mode

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,
  });
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
    }
  },
Enter fullscreen mode Exit fullscreen mode

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)