Identity Authentication
The most common identity authentication method is to use username (or email) and password to log in. This means implementing a login form so that users can log in with their personal information. The form looks like this:
<form name="loginForm" ng-controller="LoginController" ng-submit="login(credentials)" novalidate> <label for="username">Username:</label> <input type="text" id="username" ng-model="credentials.username"> <label for="password">Password:</label> <input type="password" id="password" ng-model="credentials.password"> <button type="submit">Login</button> </form>
Since this is an Angular-powered form, we use the ngSubmit directive to trigger the function when uploading the form. One thing to note is that we pass the personal information into the upload form function instead of using the $scope.credentials object directly. This makes the function easier to unit-test and reduces the coupling of the function to the current Controller scope. The Controller looks like this:
.controller('LoginController', function ($scope, $rootScope, AUTH_EVENTS, AuthService) { $scope.credentials = { username: '', password: '' }; $scope.login = function (credentials) { AuthService.login(credentials).then(function (user) { $rootScope.$broadcast(AUTH_EVENTS.loginSuccess); $scope.setCurrentUser(user); }, function () { $rootScope.$broadcast(AUTH_EVENTS.loginFailed); }); };javascript:void(0); })
We notice that there is a lack of actual logic here. This Controller is made like this to decouple the authentication logic from the form. It is a good idea to extract as much logic as possible from our Controller and put it all into services. AngularJS's Controller should only manage the objects in $scope (using watching or manual operation) instead of taking on too many overly heavy tasks.
Notify Session changes
Identity authentication will affect the state of the entire application. For this reason I prefer to use events (using $broadcast) to notify user session changes. It is a good idea to define all the event codes that may be used in a middle ground. I like to use constants to do this:
.constant('AUTH_EVENTS', { loginSuccess: 'auth-login-success', loginFailed: 'auth-login-failed', logoutSuccess: 'auth-logout-success', sessionTimeout: 'auth-session-timeout', notAuthenticated: 'auth-not-authenticated', notAuthorized: 'auth-not-authorized' })
One good feature of constants is that they can be injected into other places at will, just like services. This makes constants easily callable by our unit-test. Constants also allow you to easily rename them later without having to change a bunch of files. The same trick works with user roles:
.constant('USER_ROLES', { all: '*', admin: 'admin', editor: 'editor', guest: 'guest' })
If you want to give editors and administrators the same permissions, you simply change 'editor' to 'admin'.
The AuthService
Logic related to identity authentication and authorization (access control) is best placed in the same service:
.factory('AuthService', function ($http, Session) { var authService = {}; authService.login = function (credentials) { return $http .post('/login', credentials) .then(function (res) { Session.create(res.data.id, res.data.user.id, res.data.user.role); return res.data.user; }); }; authService.isAuthenticated = function () { return !!Session.userId; }; authService.isAuthorized = function (authorizedRoles) { if (!angular.isArray(authorizedRoles)) { authorizedRoles = [authorizedRoles]; } return (authService.isAuthenticated() && authorizedRoles.indexOf(Session.userRole) !== -1); }; return authService; })
To further distance myself from identity authentication concerns, I use another service (a singleton object, using the service style) to save the user's session information. The details of the session information depend on the back-end implementation, but I will give a more general example:
.service('Session', function () { this.create = function (sessionId, userId, userRole) { this.id = sessionId; this.userId = userId; this.userRole = userRole; }; this.destroy = function () { this.id = null; this.userId = null; this.userRole = null; }; return this; })
Once the user logs in, his information should be displayed in certain places (such as the upper right corner User avatar or something). In order to achieve this, the user object must be referenced by the $scope object, preferably one that can be called globally. Although $rootScope is the obvious first choice, I try to restrain myself from using $rootScope too much (I actually only use $rootScope for global event broadcasts). The way I prefer to do this is to define a controller at the root node of the application, or somewhere else at least higher than the DOM tree. Tags are a good choice:
<body ng-controller="ApplicationController"> ... </body>
ApplicationController is a container for the application's global logic and an option for running Angular's run method. Therefore it will be at the root of the $scope tree, and all other scopes will inherit from it (except the isolation scope). This is a good place to define the currentUser object:
.controller('ApplicationController', function ($scope, USER_ROLES, AuthService) { $scope.currentUser = null; $scope.userRoles = USER_ROLES; $scope.isAuthorized = AuthService.isAuthorized; $scope.setCurrentUser = function (user) { $scope.currentUser = user; }; })
We don't actually allocate the currentUser object, we just initialize the scoped properties so that currentUser can be accessed later. Unfortunately, we can't simply assign a new value to currentUser in the child scope because that would create a shadow property. This is the result of passing primitive types (strings, numbers, booleans, undefined and null) by value instead of by reference. To prevent shadow properties, we have to use setter functions. If you want to learn more about Angular scopes and prototypal inheritance, read Understanding Scopes.
Access control
Identity authentication, that is, access control, does not actually exist in AngularJS. Because we are a client application, all source code is in the hands of the user. There is no way to prevent users from tampering with the code to obtain an authenticated interface. All we can do is show the controls. If you need true authentication, you'll need to do it server-side, but that's beyond the scope of this article.
Limit the display of elements
AngularJS has directives to control showing or hiding elements based on scope or expressions: ngShow, ngHide, ngIf and ngSwitch. The first two will hide the element using a