How to use areas and border states to control access inside an Angular application with ui-router
UI-router is awesome!
You can create nested states and do all kinds of fancy stuff.
But what if you need to restrict access to a certain part of your application? Or what if you have a public area, a members area and an admin area in the same application?
In this article I will show you how quickly this can be accomplished with ui-router in 3 easy steps.
So stay with me and in a few minutes you'll know exactly how to control access inside your application using ui-router.
Definitions
Throughout the article I will use the following terms:
- area: a logical part of an application e.g. public area, member area, admin area
- border state: a ui-router state that is put in front of an area to control access to that area
For those of you who are familiar with networks and border routers, these concepts should sound familiar
For the sake of clarity, let's assume we have to build an e-commerce application with a public area and a private area.
STEP 1: create a map of logical areas
Restricting access to areas of your application requires a good plan.
The easiest way to create a plan is to take a sheet of paper and draw a logical map of the application where each block represents a ui-router state.
A map for our e-commerce application would look like this:
where all white rectangles are regular states, all yellow rectangles are border states and all blue rounded rectangles are areas.
So in the drawing we have 3 border states:
app
: border state for the entire applicationapp.public
: border state for public areaapp.private
: border state for private area
STEP 2: create the states in ui-router
Now that we have a plan, we can configure the states in ui-router:
angular
.module('yourApp')
.config(configureBorderStates);
function configureBorderStates($stateProvider){
$stateProvider
// Main application state
.state('app', {
abstract: true
})
// Main public state to separate public area
.state('app.public', {
abstract: true
})
// Individual public states
.state('app.public.homepage', {
url: '/homepage',
templateUrl: 'homepage/homepage.html'
})
.state('app.public.products', {
url: '/products',
templateUrl: 'products/products.html'
})
.state('app.public.about', {
url: '/about',
templateUrl: 'about/about.html'
})
// Main private state to separate private area
.state('app.private', {
abstract: true
})
// Individual private states
.state('app.private.orders', {
url: '/orders',
templateUrl: 'orders/orders.html'
})
.state('app.private.invoices', {
url: '/invoices',
templateUrl: 'invoices/invoices.html'
})
.state('app.private.settings', {
url: '/settings',
templateUrl: 'settings/settings.html'
});
}
Notice that I define the border states as abstract
. This prevents the user from navigating directly to one of the border states.
Defining the border states as abstract
is not required but a personal preference of mine to keep things clean and to use the states purely for administrative purposes, not to let the user navigate to them.
Also notice how the url's are not coupled to the state hierarchy. So even though the states are hierarchical, the url's are flat like this:
/homepage
/products
/about
/orders
/invoices
/settings
If we would assign a url to the app.private
border state:
// Main private state to separate private area
.state('app.private', {
abstract: true,
url: '/private'
})
the url's would look like this:
/homepage
/products
/about
/private/orders
/private/invoices
/private/settings
This provides you with a very powerful architecture to divide your application in logical areas while keeping solid control over the url's.
STEP 3: control access
Now that we have defined all states in ui-router, it's time to control access.
Let's register a state change event handler that checks if the user is signed in correctly when trying to access one of the private states.
Because the private states are all nicely organized under app.private
, we can do this with very little code:
angular
.module('yourApp')
.run(registerEventHandler);
/**
* Register event handler for ui-router state change
*
* See https://github.com/angular-ui/ui-router/wiki for more information.
*/
function registerEventHandler($rootScope, $state, $log, auth) {
// Fired when the transition begins
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
// When state name matches 'app.private.*'
if(toState.name && toState.name.match(/^app\.private\./)){
if(!auth.isSignedIn()){
$log.debug('Session has expired, redirect to signin page');
// Cancel state change
event.preventDefault();
// Redirect to login page
return $state.go('app.public.signIn');
}
}
});
}
// Inject dependencies;
registerEventHandler.$inject = ['$rootScope', '$state', '$log', 'auth'];
For an application with a public area, a member area and an admin area, this event handler could contain code like this:
// When state name matches 'app.member.*'
if(toState.name && toState.name.match(/^app\.member\./)){
if(!auth.isMember()){
// Cancel state change
event.preventDefault();
// Redirect to login page
return $state.go('app.public.signIn');
}
}
// When state name matches 'app.admin.*'
if(toState.name && toState.name.match(/^app\.admin\./)){
if(!auth.isAdmin()){
// Cancel state change
event.preventDefault();
// Redirect to login page
return $state.go('app.public.signIn');
}
}
Believe it or not, but that's all there is to it!
Here is a more advanced diagram of how you can structure an application when using JSON Web Tokens (JWT) to authenticate users (click the image to make it larger):
Other tactics often require you to add custom data properties to states to control access. Here we rely purely on the state hierarchy so we don't have to assign custom properties to states at all.
BONUS BENEFIT: resolve hooks for areas
If we look at our state map again, we can see that border states are perfectly placed in the hierarchy to provide us with convenient hooks to resolve specific dependencies for individual areas in your application:
If you are not familiar wiht resolving dependencies with ui-router, make sure to read this and this article.
In ui-router this is easily accomplished by adding a resolve
property to the border states:
angular
.module('yourApp')
.config(configureBorderStates);
function configureBorderStates($stateProvider){
$stateProvider
.state('app', {
abstract: true,
resolve: {
// Resolve dependencies that are
// needed in both public and private states
}
})
.state('app.public', {
abstract: true,
resolve: {
// Resolve dependencies that are
// only needed in public states
}
})
.state('app.private', {
abstract: true,
resolve: {
// Resolve dependencies that are
// only needed in private states
}
});
}
Conclusion
Using areas and border states is a very convenient way to divide your application in logical parts and control access centrally.
It requires very little code and does not require you to attach custom properties to individual states. However, if your application requires so, it can be combined effortlessly with custom state properties to apply advanced security rules.
So next time you need to secure an application, give it a shot a let me know what you think.
Have a great one!