Authorization in React with MOBX

Aleksandar Rajic
5 min readAug 21, 2020

In this article i am going to address the authorization problem in React applications.

Authorization is the function of specifying access rights/privileges to resources, which is related to information security and computer security in general and to access control in particular.

Since React is just a UI library we are going to need something to hold our global state and all necessary logic. There are couple of options here, like Redux and MOBX, but Of course my choice is MOBX. It makes reactive programming easy and straightforward, that in combination with react forms a powerful combination. This means that it can easily be abused so that all logic goes to the stores, which is wrong. Please use it wisely and leave all local logic in components, but that is subject on its own. To conclude here, MOBX comes with a number of handy features that are going to make this implementation very neat. More details on official page.

But first things first, let’s start with couple of assumptions and let’s define all necessary data structures.

We are going to have many pages and features on those pages, so not all of them are going to be public nor visible to all types of users (access groups). Most of the time there will be only one access level but let’s assume that there can be more than one. Feature can be protected by authentication, or allowed only when not authenticated, or both. Lastly, let’s assume that there can be many client apps and users can have different configurations.

As the first data structure, there is a UserInfo that contains accessGroup (role), status, accessLevel, clientId.

type UserInfo = {
accessGroup: string;
status: number;
accessLevel: AccessLevels;
clientId: number;
userId: number;
}

Depending on implementation, this information can be stored on client side or embedded into JSON Web Token. In the first case it need to be sent as a part of request when filtering out features, in case on backend filtering, or used inside authorization store, in case on client side filtering. We are going to do the second one.

AccessLevels can be defined only as numbers, but since we are not spies it is done in more readable way, by creating enum. Feel free to define as many levels as you need here.

enum AccessLevels {
Intro = 0,
Basic = 1,
Premium = 2,
}

AccessRule is a structure that has feature(key/keyPath), type, isPublic, isPrivate, isConfigurable, clientId, userId, accessGroup and accessLevel attributes.

type AccessRule = {
type: string;
key: string;
value: string;
accessGroups: string[];
isPublic: boolean;
isPrivate: boolean;
accessLevel: AccessLevels;
}
type AuthStatus = {
isAuthenticated: boolean;
token: string;
}

By using type we can filter out rules for specific type of feature, for ex. in the router store, we are going to use ‘page’ rules. For buttons, on the other hand we are going to use only ‘action’ rules. Following this our router store would be something like this.

export class Router {
@observable routes: Route[] = [];
accessControl: AccessControl;
constructor(
defaultRoutes: Route[] = [],
accessControl: AccessControl
) {
this.accessControl = accessControl;
}
@computed get allowedRoutes() {
return this.routes
.filter(route => this.accessControl.isAuthorized(route.key);
}
// used to fill out rules once they are loaded from db
@action notify(routes = []) {
this.availableRoutes = routes
.filter(({ rule_type: type }) => type === 'page');
}
}

At the beginning, there is filtering of routes that are available just by their authentication rule. If user is authenticated he should be seeing both public and private routes, with the exception of routes that are reserved only for unauthenticated mode like login, register, etc. Lastly, there is authorization check by role so that user can see only those that are allowed for his user type or/and access level.

AccessControl class, is going to be the brain of authorization module. This class has references to AuthStatus, AccessRule and UserInfo observables, that are injected through constructor and isAuthorized method, that is doing all heavy lifting.

class AccessControl {
@observable authStatus: AuthStatus;
@observable accessRules: AccessRule[];
@observable userInfo: UserInfo;
@observable observers: any[] = []; constructor(authStatus, defaultAccessRules, userInfo) {
this.authStatus = authStatus;
this.accessRules = defaultAccessRules;
this.userInfo = userInfo;
}
@action loadRules() {
// load them from db filter by any criteria you need
// and finally, notify all subscribed stores
}
@action subscribe(observer) {
this.observers.push(observer);
}
isAuthorized(rule: string): boolean {
// implementation follows
}
@computed get authorizedFeatures() {
// implementation follows
}
}

Finally we are coming to the most important part of our logic, isAuthorized function. Authorization is done in three steps:

  • Checking if authentications is done and if feature is allowed in authenticated or non authenticated mode
  • Checking if user’s access group has access to the feature
  • Checking if access level of a user is not less than access level of a rule
isAuthorized(rule: string): boolean {
const accessRule = this.authorizedFeatures
.find(({ feature }) => feature === rule);
if (!accessRule) {
// allow invalid access rule?
// depending of implementation,
// here, an error may be thrown
// or feature may be allowed,
// up to you...
// in my case it is false
return ALLOW_UNKNOWN_ACCESS_RULES;
}
// The first level authorization
const level1 = this.authStatus.isAuthenticated
&& accessRule.is_authenticated
&& !accessRule.is_unauthenticated;
|| !this.authStatus.isAuthenticated
&& route.is_unauthenticated;
// The second level authorization
const level2 = !accessRule.accessGroups
|| !accessRule.accessGroups.includes(userInfo.accessGroup);
// The third level authorization
const level3 = userInfo.accessLevel <= accessRule.accessLevel;
return level1 && level2 && level3;
}

Rules can also be loaded from the db, but here we are using set of default rules.

I am not going to talk about MOBX and its automatically tracking of changes but do know that since we are working with observables, components that are wrapped with withAuthorization will be re-rendered automatically.

Next, we have created necessary classes but now it is time to initialise them. The most important

import { createContext, useContext } from 'react';interface Stores = {
accessControl: AccessControl,
router: Router,
}
const initStores = (): Stores => {
const defaultRules: AccessRule[] = [
// my default set of rules...
];

const authStatus: AuthStatus = {
isAuthenticated: false,
token: null,
};
const userInfo = {
accessGroup: null,
status: null,
accessLevel: null,
};
const accessControl = new AccessControl(
authStatus,
defaultRules,
userInfo,
);
const router = new Router(
defaultRules,
accessControl,
),
accessControl.subscribe(router);
accessControl.loadRules();
return {
accessControl,
router,
};
};
const storesContext = createContext(initStores());
const useStores = (): Stores => useContext(storesContext);

In the end, we are creating withAuthorization that is accepting Component and rule params and that is returning ObserverComponent. This component is subscribed to accessControl store and it is tracking if there are some changes and if feature should be rendered or not;

export const withAccessControl = (Component, keyPath) => {
const ObserverComponent = observer(() => {
const { accessControlRoles } = useStores();
if (!accessControlRoles.checkPermission(keyPath)) {
return null;
}
return <Component />;
});
return <ObserverComponent key={keyPath} />
};
const useAuthorization = observer((keypath) => {
const { accessControl } = useStores();

return accessControl.checkPermissions(keypath);
});

Using MOBX on couple of projects made me fall in love with it and move forward from Redux without turning back. This module was very easy to implement and integrate with it and depending on your needs it can be simpler or more complex, up to you.

This is just a brief overview of how it should be done, in my opinion. So far i was using this approach in couple of projects with slightly different forms and it turns out it is worked really well. So i am sharing it with the world.

Please share with me ideas how to improve it further and have a nice day.

--

--

Aleksandar Rajic

Tech Lead at Manigo & Writter at Software Engineering Crafted