Creating an authentication system within a Single Page App (SAP) can be a bit tricky; hopefully this post will help alleviate some of the challenges. Before you begin, it's a good idea to set up your environment so as to decouple the front and backends. There's a good post explaining the process over at StackOverflow. (The StackOverflow answer deals with a Laravel backend, but the code is easily modified for any other backend service.)
Our application will be structured as follows:
auth_demo/ - application root
auth_demo/backend/ - backend (PHP app) root
auth_demo/frontend/ - frontend (AngularJS app) root
auth_demo/frontend/index.html - AngularJS index
auth_demo/frontend/js/ - JavaScript libraries
auth_demo/frontend/js/app/app.js - core AngularJS application
auth_demo/frontend/js/app/controllers/ - AngularJS controllers
auth_demo/frontend/js/app/directives/ - AngularJS directives
auth_demo/frontend/js/app/partials/ - AngularJS partials
auth_demo/frontend/js/app/services - AngularJS services
Let's start with our basic index.html setup:
<!DOCTYPE html>
<html data-ng-app="auth_demo">
<head>
<title>Auth Demo</title>
</head>
<body>
<div class="user-info" data-ng-show="user_email">
| <a href="" data-ng-click="logout()">Logout</a>
</div>
<div data-ng-view></div>
<script type="text/javascript" src="js/angular.min.js"></script>
<script type="text/javascript" src="js/angular-route.js"></script>
<script type="text/javascript" src="app/app.js"></script>
</body>
</html>
There are a few things going on here... First, we declare this DOM as an AngularJS app named "auth_demo." Second, we create a user-info
DIV (which will only be shown when the ng-show
directive's expression evaluates to true
), bind the user_email
variable, and provide a logout link which will trigger the logout()
function on click. Finally, we use the ng-view
directive to display the content for our menu route, and then load the various JavaScript files required by our app.
Now we can create our basic App (in app.js), routing a few paths to appropriate controllers and partials:
var app = angular.module('auth_demo', [
'ngRoute'
]);
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
controller: 'LoginController',
templateUrl: '/app/partials/login.html',
resolve: {
factory: isAnon
}
})
.when('/user', {
controller: 'UserController',
templateUrl: '/app/partials/user.html',
resolve: {
factory: isAuth
}
})
.otherwise({
redirectTo: '/'
});
});
Note that we declare ngRoute
as a dependency for our App, and use the resolve
object property to map dependencies that will be injected into the controller. Our isAnon
and isAuth
dependencies will be promises, and Angular's router will wait for them to be resolved or rejected before instantiating the controller. This means we can use them as a sort of "filter" to control access to our routes.
Now we will create the isAuth
and isAnon
promises:
/**
* Ensures user is authenticated, otherwise redirects to /
* @param {[type]} $q [description]
* @param {[type]} $http [description]
* @param {[type]} $rootScope [description]
* @param {[type]} $location [description]
* @return {Boolean} [description]
*/
var isAuth = function($q, $http, $rootScope, $location) {
var defered = $q.defer();
$http.get('/ws/api/v1/auth', { }).success(function (data) {
if (data.status !== 'ok') {
return;
}
// user is authenticated
if (data.auth === 1) {
$rootScope.user_email = data.user_email;
defered.resolve(true);
}
// user is anonymous
else {
defered.reject();
$location.path('/');
}
});
return defered.promise;
};
/**
* Ensures user is anonymous, otherwise redirects to /user
* @param {[type]} $q [description]
* @param {[type]} $http [description]
* @param {[type]} $rootScope [description]
* @param {[type]} $location [description]
* @return {Boolean} [description]
*/
var isAnon = function($q, $http, $rootScope, $location) {
var defered = $q.defer();
$http.get('/ws/api/v1/auth', { }).success(function (data) {
if (data.status !== 'ok') {
return;
}
// user is anonymous
if (data.auth === 0) {
defered.resolve(true);
}
// user is authenticated
else {
defered.reject();
$location.path('/user');
}
});
return defered.promise;
};
Both of the promises perform a GET
request against /ws/api/v1/auth
and work with the returned JSON. Our example backend will return a JSON object which will contain
a string status
property (set to either ok
or error
), a boolean auth
property (current authentication state), and a string user_email
property. Depending on the values in returned JSON, the promises will either resolve or reject.
Now that the promises are set up (and the backend is returning the appropriate JSON response) we can create the login form in partials/login.html:
<form id="login-form" class="partial-popup" data-ng-submit="process()">
<h1>Login</h1>
<messages-list></messages-list>
<input type="text" name="email" placeholder="email" data-ng-model="form_data.email" />
<input type="password" name="password" placeholder="password" data-ng-model="form_data.password" />
<input type="submit" value="Login" />
</form>
This form creates two textfields, binds their values to a form_data
object, and calls
the process()
function upon form submission.
Next up is the the loginController
we refenced in our route provider. Declare it in
controllers/loginController.js, and don't forget to load the file in your index.html.
You'll note that the controller uses a (so far undefined) service called authService. Let's create it in services/authService.js...