Original cover photo by Sandy Millar on Unsplash.
What is access restricting?
Restricting access in an application is essentially modifying how different users may interact with our platform. Some users may have permissions to see some pages and some might not; authenticated users may be able to visit certain segments of an app, and maybe not able to visit others; or it can be more granular, like disabling sections in pages that are otherwise available, or even performing some actions, but not in their entirety. Today, we are going to examine how this can be done in the most convenient way using NgRx in Angular apps.
What tools do we have?
First of all, we have to keep in mind, that Angular itself already provides some pretty powerful tools hat help us achieve access restriction. Let's first reiterate them:
- Guards - special classes that restrict access to pages via routing based on a condition we provide
- Interceptors - while they do not come as specifically targeting access restriction, they can be used to modify and even prevent network requests
- NgIf directive - yes, this is our main tool when dealing with granular access restriction
- NgSwitch directive - main tool when we need to provide different components to the end user based on some role
Aim of this article is not to try and replace these tools with NgRx, but rather make our reactive store interplay with these tools to achieve best UX with as little and concrete code as possible.
What are our use cases?
We are going to show access restriction on three main scenarios:
- Authentication with NgRx
- Handling permissions
- Showing certain features depending on roles/permissions stored in NgRx Store
Let's begin with authorization.
Authorization with NgRx
When dealing with apps that use NgRx, the most important thing to remember is not to duplicate data and keep our interface access to the Store data simple and efficient. For this, we need to understand what sort of scenario we are dealing with. For the purpose of this example, we will implement authorization with following characteristics:
- Using a JSON Web Token
- Storing the token in cookies
- Retrieving the user data using the token for a call to a special API
- Checking the auth status of the user
- Getting user info (for a profile page or for displaying somewhere)
This is a somewhat specific scenario, but it can be easily extrapolated to other use cases, as in using localStorage
instaead of cookies
(keep in mind that localStorage
is considered insecure), or using an entirely different authorization strategy.
Now let's understand what sort of data we would need to keep in the Store. At first glance, we will probably need to store the following:
- Current user data
- a boolean indicating if the user is logged in
- the auth token itself
Seems alright, doesn't it? Wrong!. We do not need to store anything other than the user data itself, because:
- The existence of user data already proves the user is logged in
- The token itself contains the user data, just needs to be decoded
- The call to API will be made immediately when the app starts
So we will just store the user data in our auth
section of the store, and write several selectors that access the logged in state, the token itself, and decoded user data. So our AuthState
will look very simple, like this:
export interface AuthState {
token: string;
user: User;
}
export const initialState: AuthState = {
token: "",
user: null,
};
Now, we need actions to put the token into the state, remove it and so on:
import { createAction, props } from "@ngrx/store";
export const setToken = createAction(
"[Auth] Set Token",
props<{ token: string }>()
);
export const setUser = createAction(
"[Auth] Set user",
props<{ user: User }>(),
);
export const removeToken = createAction("[Auth] Remove Token");
And then a simple reducer which will handle interactions:
import { createReducer, on } from "@ngrx/store";
import { removeToken, setToken } from "./actions";
import { AuthState, initialState } from "./state";
export const authReducer = createReducer(
initialState,
on(setToken, (state, { token }): AuthState => ({ ...state, token })),
on(removeToken, (state): AuthState => ({ ...state, token: "" })),
on(setUser, (state, { user }): AuthState => ({ ...state, user }))
);
Now, let's register this in our AppModule
:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";
import {
UserDashboardComponent,
} from "./user-dashboard/user-dashboard.component";
import { AppRoutingModule } from "./routing.module";
import { LoginComponent } from "./login/login.component";
import { StoreModule } from "@ngrx/store";
import { authReducer } from "./store/reducer";
import { CommonModule } from "@angular/common";
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule,
CommonModule,
StoreModule.forRoot({ auth: authReducer }),
],
declarations: [AppComponent, UserDashboardComponent, LoginComponent],
bootstrap: [AppComponent],
})
export class AppModule {}
Now we need to write the selectors to pick up the data we want:
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { AuthState } from "./state";
import { decode } from "some-jwt-library";
export const authFeature = createFeatureSelector<AuthState>("auth");
export const selectToken = createSelector(
authFeature,
(state) => state.token,
);
export const selectIsAuth = createSelector(
authFeature,
(state) => !!state.token
);
export const selectUserData = createSelector(
authFeature,
(state) => state.user
);
And that's it for the initial part, we now can store the data about the user and retrieve it safely. Now let's discuss how we get that data into the store. For this, we will use three effects:
- Log in: send the login data to backend, get the token, store in cookies and then the Store itself
- Log out: remove the token, redirect to the login page
- Retrieve token: when the app; starts, check the cookies for a token and store it in the Store
First we add relevant actions:
export const login = createAction(
"[Auth] Login",
props<{ email: string; password: string }>()
);
export const loginError = createAction(
"[Auth] Login",
props<{ message: string }>()
);
export const logout = createAction("[Auth] Log Out");
And the effects themselves. Pay special attention to the last one:
@Injectable()
export class AuthEffects {
// on login, send auth data to backend,
// get the token and put into the store and cookies
login$ = createEffect(() => {
return this.actions$.pipe(
ofType(login),
mergeMap(({ email, password }) => {
return this.authService.login(email, password).pipe(
tap(({ token }) => this.cookieService.set("token", token)),
map(({ token }) => setToken({ token })),
catchError(() => of(loginError({ message: "Login failed" })))
);
})
);
});
// on logout, just remove the token
// and navigate to login page
// no need to dispatch any actions after that
logout$ = createEffect(
() => {
return this.actions$.pipe(
ofType(logout),
tap(() => {
this.cookieService.remove("token");
this.router.navigateByUrl("/login");
})
);
},
{ dispatch: false }
);
// when app has started, get the user data
// using the token from cookies
// and put into the store
init$ = createEffect(() => {
return this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
mergeMap(({ email, password }) => {
return this.authService.getCurrentUser().pipe(
map(({ token }) => setUser({ user })),
catchError(() => of(setUserError({ message: "Error" })))
);
})
);
});
constructor(
private readonly actions$: Actions,
private readonly authService: AuthService,
private readonly router: Router,
private readonly cookieService: CookieService
) {}
}
First two effects are fairly straightforward, so let's focus on the last one. Here, we have taken advantage of the built-in ROOT_EFFECTS_INIT
action which gets dispatched by NgRx itself when it subscribes to our effects - essentially acting as a notifier for our app start in this scenario. Then we put the user data into our state, essentially rehydrating it.
So now we have our auth in NgRx ready, the final addition will be creating a special guard that will check the existence of our auth token:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly store: Store,
private readonly router: Router,
) {}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) {
return this.store.select(selectIsAuth).pipe(
map((isAuth) => {
return isAuth ? true : this.router.parseUrl("/login");
})
);
}
}
Essentially, all the logic behind autgorization has been delegated to the NgRx Store, and the guard just asks the Store whether to proceed or not.
This is mostly it. Now, let's discuss permission based access restrictions.
Handling permissions with NgRx
Different applications have different approaches to permissions, different ways to store them and validate their existence. The cool thing about NgRx is that we do not care what is the shape of the permissions - we can always write selectors that provide a similar interface for checking permissions and granting access regardless of what the permissions look like.
For the sake of simplicity, for this example we will imagine that the permissions in our app are stored in the JWT token together with the user data in an array of strings - if the name of the permission in present then the user has that permission, otherwise not. So, our user object might look something like this:
interface UserData {
firstName: string;
lastName: string;
permissions: string[];
}
Now, let's add the relevant selectors:
export const selectPermissions = createSelector(
selectUserData,
(userData) => userData?.permissions ?? []
);
Now this will return the array that we have, but in certain cases we need to check the existence of a particular permission. For this, we can write a selector with an argument:
export const selectHasPermission = (permission: string) =>
createSelector(selectPermissions, (permissions) =>
permissions.includes(permission)
);
Then in our components we can use this selector to check whether the user has the permission to perform a certain action:
export class UserDashboardComponent implements OnInit {
canCreateOrder$ = this.store.select(
selectHasPermission("CreateOrder"),
);
constructor(private readonly store: Store) {}
}
And then in the template:
<button *ngIf="canCreateOrder$ | async" (click)="createOrder()">
Create Order
</button>
Starting from Angular 14, using the inject
function, we can create a customizable functional guard for permission checkings:
export function hasPermissionGuard(permission: string) {
return function () {
const store = inject(Store);
return store.select(selectHasPermission(permission));
};
}
And then we can use it in our routes:
const routes: Routes = [
{
path: "orders",
component: OrdersComponent,
canActivate: [hasPermissionGuard("ViewOrders")],
},
{
path: "orders/create",
component: CreateOrderComponent,
canActivate: [hasPermissionGuard("CreateOrder")],
},
];
Note: we can customize the guard to accept multiple permissions and check whether the user has all of them, and also add another argument to handle redirects if needed.
Bonus point: finally, we visit the section where we provide granular access to certain parts of the UI:
Handling granular access to UI elements
For this, can create a directive that will hide the element if the user does not have the permission. This directive will:
- Be a structural directive -
*appHasPermission="permission"
- Take a string as an input - the name of the permission
- Use the
Store
to access permissions - Use the
selectHasPermission
selector to check whether the user has the permission - Hide the element if the user does not have the permission
- Show the element if the user has the permission
- Unsubscribe from the store when the element is destroyed
Here we go:
@Directive({
selector: "[appHasPermission]",
})
export class HasPermissionDirective implements OnInit, OnDestroy {
@Input("appHasPermission") permission: string;
destroy$ = new Subject<void>();
constructor(
private readonly templateRef: TemplateRef<any>,
private readonly viewContainer: ViewContainerRef,
private readonly store: Store
) {}
ngOnInit() {
this.store
.select(selectHasPermission(this.permission))
.pipe(takeUntil(this.destroy$))
.subscribe((hasPermission) => {
if (hasPermission) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}
ngOnDestroy() {
this.destroy$.next();
}
}
And then in the template:
<a *appHasPermission="'CreateOrder'" routerLink="/orders/create">
Create Order
</a>
Conclusion
Access restriction has a very improtant role in multiple applications, especially enterprise apps. NgRx is a great tool for handling authorization and permissions, and it can be used in a variety of ways. In this article, we have covered the most common use cases, but there are many more, so never hesitate to explore new ways of doing things with NgRx.
Top comments (6)
Thank you for the insightful article; I found the concept very interesting. I have a couple of questions regarding this approach:
Does this method violate SOLID principles?
My understanding is that state management is intended for handling the application's state, not for directly managing access permissions. For example, I believe it would be better to create a centralized service dedicated to access control. This service would determine whether to obtain permissions from the store, a BehaviorSubject (which also allows synchronous data retrieval), or other sources.
Will integrating permission checks directly into the state manager complicate component testing?
Great article and examples. One thing though: I do think that you should store a boolean indicating if the user is logged in, because even if the token is present (e.g. rehydrated from cookies or localStorage), it might already be expired which prevents the user from conducting authenticated actions, thus not logged in.
@deinding it differs, you can write or import a function that validated the token expiration and use it on the selector, or maybe not store the token and only rely on HttpOnly cookies.
Anyway, thanks for appreciation :)
really cool usecases as always
Can you make an example with a functional route guard using an ngrx store?
Great.
Where can i found the full source code for this tuto ? Please