Understanding Access Restriction
Access restriction refers to controlling how different users interact with a platform. Certain users may have permission to view specific pages while others do not; authenticated users might access some parts of an app but be blocked from others. On a finer level, restriction can involve disabling sections within otherwise available pages, or limiting certain actions without removing them entirely. This article explores how to implement these patterns efficiently with NgRx in Angular applications.
Available Tools
Angular itself provides solid built-in capabilities for access restriction. Let's review them:
- Guards — classes that control route access based on given conditions
- Interceptors — not specifically designed for access restriction, but capable of modifying or blocking network requests
- NgIf directive — primary tool for granular access control
- NgSwitch directive — ideal for rendering different components based on user roles
Rather than replacing these tools with NgRx, our goal is to make the reactive store work alongside them, producing a clean, concise, and high-quality user experience.
Our Use Cases
We'll demonstrate access restriction through three primary scenarios:
- Authentication implemented with NgRx
- Permission management
- Feature visibility determined by roles or permissions held in the NgRx Store
Starting with authorization.
Authorization with NgRx
In apps leveraging NgRx, avoiding data duplication is essential, along with keeping the Store interface simple and efficient. Clear understanding of the situation we're dealing with helps here. For this example, our authorization scheme works as follows:
- Relies on a JSON Web Token
- The token resides in cookies
- User information is fetched through a dedicated endpoint using the token
- The authentication state is checked
- User details are retrieved (for a profile view or for display elsewhere)
Though this scenario is fairly tailored, it maps well to alternative approaches, such as employing localStorage rather than cookies (notably, localStorage is regarded as insecure), or using a fundamentally different authorization method.
Let's determine what data belongs in the Store. At first glance, these candidates come to mind:
- Current user data
- A flag signaling whether the user is authenticated
- The authentication token
Reasonable, right? Not quite! Only the user data itself needs to reside in the Store, because:
- Presence of user data already indicates that the user is authenticated
- The token carries user information within itself—just decode it
- The API call happens right at application startup
Consequently, we store only the user data in the auth slice of the Store. Several selectors will then expose login state, the token, and the decoded user details. The resulting AuthState is remarkably compact:
export interface AuthState {
token: string;
user: User;
}
export const initialState: AuthState = {
token: "",
user: null,
};
Now, we require actions for placing the token into the state, removing it, and similar operations:
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");
Followed by a minimal reducer that processes these 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 }))
);
Let's now set this up inside the 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 {}
With the state in place, we'll write selectors that retrieve the required data:
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
);
That completes the foundational setup—we can now store user data and retrieve it safely. The next question is: how does data get into the Store? Three effects handle this:
- Sign in: sends credentials to the backend, receives a token, saves it to cookies and the Store
- Sign out: deletes the token and navigates to the login page
- Token restoration: at application start, checks cookies for a token and loads it into the Store
Let's add the necessary actions first:
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 now the effects themselves—pay particular attention to the final 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
) {}
}
The first two effects are quite simple, so let's examine the last one closely. Here, we leverage the built-in ROOT_EFFECTS_INIT action—NgRx dispatches this automatically when it subscribes to our effects, effectively telling us that the app is starting. We then write the user data back into the state, essentially rehydrating it.
With auth fully wired through NgRx, the final piece is a guard that verifies the existence of the 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");
})
);
}
}
In essence, all authorization logic now lives in the NgRx Store, and the guard merely consults the Store to decide whether to proceed.
That covers the authorization side. Next, let's move on to permission-based access restrictions.
Implementing Permissions With NgRx
Applications vary widely in how they manage permissions—where they store them and how they confirm their presence. The advantage of NgRx here is that the underlying structure of permissions doesn't matter. We can craft selectors that expose a consistent interface for verifying permissions and granting access, no matter the shape of the data.
To keep this example straightforward, let's assume our permissions live inside the JWT token alongside user information. They're stored as an array of strings—if the permission name appears in the array, the user has it; otherwise, they don't. So, the user object might resemble this:
interface UserData {
firstName: string;
lastName: string;
permissions: string[];
}
Next, we introduce the selectors:
export const selectPermissions = createSelector(
selectUserData,
(userData) => userData?.permissions ?? []
);
This gives us the raw array, but often we need to verify the presence of a specific permission. For that, we can write a selector that accepts an argument:
export const selectHasPermission = (permission: string) =>
createSelector(selectPermissions, (permissions) =>
permissions.includes(permission)
);
Our components can then tap into this selector to determine whether the user is authorized for a given action:
export class UserDashboardComponent implements OnInit {
canCreateOrder$ = this.store.select(
selectHasPermission("CreateOrder"),
);
constructor(private readonly store: Store) {}
}
And in the template:
<button *ngIf="canCreateOrder$ | async" (click)="createOrder()">
Create Order
</button>
With Angular 14 and above, the inject function allows us to build a reusable functional guard for authorization checks:
export function hasPermissionGuard(permission: string) {
return function () {
const store = inject(Store);
return store.select(selectHasPermission(permission));
};
}
We can then apply this guard to our routes:
const routes: Routes = [
{
path: "orders",
component: OrdersComponent,
canActivate: [hasPermissionGuard("ViewOrders")],
},
{
path: "orders/create",
component: CreateOrderComponent,
canActivate: [hasPermissionGuard("CreateOrder")],
},
];
Note: the guard can be extended to accept multiple permissions and check if the user holds all of them, and we could also add an extra parameter to factor in redirects when necessary.
Bonus point: We now reach the point where we restrict access to distinct areas of the interface:
Fine-Grained Control Over UI Components
For this, we can put together a directive that hides an element when the user lacks the required permission. This is what the directive will do:
- Work as a structural directive —
*appHasPermission="permission" - Accept a string input representing the permission name
- Access permissions via the
Store - Employ the
selectHasPermissionselector to verify the user's authorization - Conceal the element when the permission is missing
- Display the element when the permission is present
- Clean up the store subscription when the element gets destroyed
Let's implement it:
@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>
Wrapping Up
Controlling access plays a vital role in many applications, particularly in enterprise settings. NgRx proves to be a robust solution for managing authentication and permissions, and it offers a wide range of possibilities. We've explored the most typical scenarios here, yet countless others exist—so feel free to experiment and discover fresh approaches with NgRx.
