<!DOCTYPE html>
<html lang="en" ng-app="demo" id="ng-app">
<head>
<link data-require="bootstrap-css@3.x" data-semver="3.0.3" rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" />
<script data-require="angular.js@*" data-semver="1.2.9" src="http://code.angularjs.org/1.2.9/angular.js"></script>
<script data-require="ui-bootstrap@*" data-semver="0.10.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.10.0.js"></script>
<script data-require="ui-router@*" data-semver="0.2.8" src="http://angular-ui.github.io/ui-router/release/angular-ui-router.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<h1>UI-Router Authorization Demo</h1>
<div ui-view="content">
Please wait while we retrieve your user info
</div>
<hr />
<p>This demo covers how to resolve principal and identity info when your app starts up, and then perform authorization checks during state changes to make sure they have access as explained in my <a href="http://stackoverflow.com/questions/19721125/resolve-http-request-before-running-app-and-switching-to-a-route-or-state" target="_blank">SO question.</a>
</p>
<h2>What's the Problem?</h2>
<p>
It is easy to check some data during the <code>$stateChangeStart</code>
event that ui-router broadcasts, assuming it has already been resolved and is immediately available by the time you respond to the event.
</p>
<p>Let's say, for example, we need to retrieve a list of roles the user is a member of. If the user goes to the site for the first time and logs in, you can authenticate them, send back the data on the user, and store it somewhere such a service. Your event handler can then use this data to do an authorization check as they move from state to state. If they fail authorization, send them to a log in screen or whatever.</p>
<p>The problem lies in the fact that said workflow requires the user to log in every time. Once they log in through the UI, they are fine. But if they refresh the browser, or type in a path in the browser manually, or follow a link from somewhere else, such as an email, your app will default to the "unauthenticated" state and they'll need to sign in again. That's no good. It's especially frustrating if you authenticate a user for a long period of time, say, by a cookie. When they revist the site, they just want to be logged in!</p>
<p>You would solve this by retrieving the same data you normally send during log in, only you request it on their behalf using some other claim. I already mentioned the cookie example. I might make a quick request using <code>$http</code>
an endpoint that authenticates the user's identity from the cookie, and respond with the info we need like we do for log in. We can then stash that data in the service we mentioned earlier and go from there.</p>
<p>That means, however, that we need this resolution to occur when your app runs, but before you are routed to any states. If you don't already know, Angular (and thus ui-router) uses an asynchronous pattern that is highly dependent on promises for execution. Therefore, it's not practical to block execution until your start-up authentication stuff is done.</p>
<p>We'll need to use some trickery to make sure required resources are resolved before we enforce any authorization rules with our states. </p>
</body>
</html>
'use strict';
angular.module('demo', ['ui.router', 'ui.bootstrap'])
// principal is a service that tracks the user's identity.
// calling identity() returns a promise while it does what you need it to do
// to look up the signed-in user's identity info. for example, it could make an
// HTTP request to a rest endpoint which returns the user's name, roles, etc.
// after validating an auth token in a cookie. it will only do this identity lookup
// once, when the application first runs. you can force re-request it by calling identity(true)
.factory('principal', ['$q', '$http', '$timeout',
function($q, $http, $timeout) {
var _identity = undefined,
_authenticated = false;
return {
isIdentityResolved: function() {
return angular.isDefined(_identity);
},
isAuthenticated: function() {
return _authenticated;
},
isInRole: function(role) {
if (!_authenticated || !_identity.roles) return false;
return _identity.roles.indexOf(role) != -1;
},
isInAnyRole: function(roles) {
if (!_authenticated || !_identity.roles) return false;
for (var i = 0; i < roles.length; i++) {
if (this.isInRole(roles[i])) return true;
}
return false;
},
authenticate: function(identity) {
_identity = identity;
_authenticated = identity != null;
},
identity: function(force) {
var deferred = $q.defer();
if (force === true) _identity = undefined;
// check and see if we have retrieved the identity data from the server. if we have, reuse it by immediately resolving
if (angular.isDefined(_identity)) {
deferred.resolve(_identity);
return deferred.promise;
}
// otherwise, retrieve the identity data from the server, update the identity object, and then resolve.
// $http.get('/svc/account/identity', { ignoreErrors: true })
// .success(function(data) {
// _identity = data;
// _authenticated = true;
// deferred.resolve(_identity);
// })
// .error(function () {
// _identity = null;
// _authenticated = false;
// deferred.resolve(_identity);
// });
// for the sake of the demo, fake the lookup by using a timeout to create a valid
// fake identity. in reality, you'll want something more like the $http request
// commented out above. in this example, we fake looking up to find the user is
// not logged in
var self = this;
$timeout(function() {
self.authenticate(null);
deferred.resolve(_identity);
}, 1000);
return deferred.promise;
}
};
}
])
// authorization service's purpose is to wrap up authorize functionality
// it basically just checks to see if the principal is authenticated and checks the root state
// to see if there is a state that needs to be authorized. if so, it does a role check.
// this is used by the state resolver to make sure when you refresh, hard navigate, or drop onto a
// route, the app resolves your identity before it does an authorize check. after that,
// authorize is called from $stateChangeStart to make sure the principal is allowed to change to
// the desired state
.factory('authorization', ['$rootScope', '$state', 'principal',
function($rootScope, $state, principal) {
return {
authorize: function() {
return principal.identity()
.then(function() {
var isAuthenticated = principal.isAuthenticated();
if ($rootScope.toState.data.roles && $rootScope.toState.data.roles.length > 0 && !principal.isInAnyRole($rootScope.toState.data.roles)) {
if (isAuthenticated) $state.go('accessdenied'); // user is signed in but not authorized for desired state
else {
// user is not authenticated. stow the state they wanted before you
// send them to the signin state, so you can return them when you're done
$rootScope.returnToState = $rootScope.toState;
$rootScope.returnToStateParams = $rootScope.toStateParams;
// now, send them to the signin state so they can log in
$state.go('signin');
}
}
});
}
};
}
])
.controller('SigninCtrl', ['$scope', '$state', 'principal', function($scope, $state, principal) {
$scope.signin = function() {
// here, we fake authenticating and give a fake user
principal.authenticate({
name: 'Test User',
roles: ['User']
});
$state.go($scope.returnToState.name, $scope.returnToStateParams);
}
}
])
.controller('HomeCtrl', ['$scope', '$state', 'principal',
function($scope, $state, principal) {
$scope.signout = function() {
principal.authenticate(null);
$state.go('signin');
};
}
])
.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/');
$stateProvider.state('site', {
'abstract': true,
resolve: {
authorize: ['authorization',
function(authorization) {
return authorization.authorize();
}
]
}
}).state('home', {
parent: 'site',
url: '/',
data: {
roles: ['User']
},
views: {
'content@': {
templateUrl: 'home.html',
controller: 'HomeCtrl'
}
}
}).state('signin', {
parent: 'site',
url: '/signin',
data: {
roles: []
},
views: {
'content@': {
templateUrl: 'signin.html',
controller: 'SigninCtrl'
}
}
}).state('restricted', {
parent: 'site',
url: '/restricted',
data: {
roles: ['Admin']
},
views: {
'content@': {
templateUrl: 'restricted.html'
}
}
}).state('accessdenied', {
parent: 'site',
url: '/denied',
data: {
roles: []
},
views: {
'content@': {
templateUrl: 'denied.html'
}
}
});
}
])
.run(['$rootScope', '$state', '$stateParams', 'authorization', 'principal',
function($rootScope, $state, $stateParams, authorization, principal) {
$rootScope.$on('$stateChangeStart', function(event, toState, toStateParams) {
$rootScope.toState = toState;
$rootScope.toStateParams = toStateParams;
if (principal.isIdentityResolved()) authorization.authorize();
});
}
]);
/* Styles go here */
This is the home page content. If you see this, you are logged in.
<br />
<a href="" ui-sref="restricted">Go to restricted page (will show you Access Denied)</a>
<br />
<a href="" ng-click="signout()">Click to sign out</a>
You must sign in to view the content (just click the button). <button type="button" class="btn btn-primary" ng-click="signin()">Sign In</button>
Should not see me.
<alert type="'danger'">
<strong>Access Denied</strong>
<p>You don't have permission to see this. <a href="" ui-sref="home">Return home.</a></p>
</alert>