Now that we have a better understanding of client-side security and its drawbacks, let's put it into practice by developing an app with the following features:
Let's start with the configuration of our basic project structure. If you have read the book until this point, this should be second nature to you by now! Go to a desired project directory, and from there, just run the following from your terminal or command line:
ionic start secureApp
This will create a basic, blank Ionic app. Let's add some basic structure to it. The first thing that we want to do is add two basic navigation states—home and public. Navigate to your app's www/js
folder and make sure that app.js
has the following:
angular.module('secureApp', []) .run(function ($ionicPlatform) { $ionicPlatform.ready(function () { // Hide the accessory bar by default (remove this to show // the accessory bar above the keyboard for form inputs) if (window.cordova && window.cordova.plugins.Keyboard) { cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); } if (window.StatusBar) { // org.apache.cordova.statusbar required StatusBar.styleDefault(); } }); }) .config(function ($stateProvider, $urlRouterProvider) { $stateProvider .state('app', { url: "/app", abstract: true, templateUrl: "templates/menu.html" }) .state('app.home', { url: "/home", views: { 'menuContent': { templateUrl: "templates/home.html" } } }) .state('app.private', { url: "/private", views: { 'menuContent': { templateUrl: "templates/private.html" } } }); // if none of the above states are matched, use this as the fallback $urlRouterProvider.otherwise('/app/home'); });
This will set up the essential navigation states for the app, which fortunately are very few at this point! However, we still need to add the necessary templates. Inside the www
directory, create a templates
directory and add the following three files to the path www/templates/menu.html
:
<ion-side-menus enable-menu-with-back-views="false"> <ion-side-menu-content> <ion-nav-bar class="bar-stable"> <ion-nav-back-button></ion-nav-back-button> <ion-nav-buttons side="left"> <button class="button button-icon button-clear ion-navicon" menu-toggle="left"> </button> </ion-nav-buttons> </ion-nav-bar> <ion-nav-view name="menuContent"></ion-nav-view> </ion-side-menu-content> <ion-side-menu side="left"> <ion-header-bar class="bar-stable"> <h1 class="title">Left</h1> </ion-header-bar> <ion-content> <ion-list> <ion-item menu-close href="#/app/home"> Home </ion-item> <ion-item menu-close href="#/app/private"> Private </ion-item> </ion-list> </ion-content> </ion-side-menu> </ion-side-menus>
The following code snippet represents the home.html
templates at the path www/templates/home.html
:
<ion-view view-title="Search"> <ion-content class="has-header"> <h1>A secure app!</h1> <div class="card"> <div class="item item-text-wrap"> This app contains extremely secretive confidential mustneverbeseen-ish information that will cause a disaster if it leaks out. It will also kill all dolphins. Please save the dolphins. </div> </div> </ion-content> </ion-view>
The following code snippet represents the private.html
templates at the path www/templates/private.html
:
<ion-view view-title="Search"> <ion-content class="has-header"> <h1>Secret content!</h1> <div class="card"> <div class="item item-text-wrap"> You are authorized to see the grand secrets of the Universe! </div> </div> </ion-content> </ion-view>
That's all that we need for the basic setup. You can verify it by running the following in a terminal or command line in the root
folder of your directory:
Ionic serve -l
You will see the following:
A dire warning indeed! Let's see if we can get around it. If you click on the app icon at the top left of the app screen (either for Android or iOS), you can bring out the navigation drawer that we created in the www/templates/menu.html
file:
If you select the Private link from the list, you would expect the app to stop us from accessing information that could potentially put an end to Flipper once and for all, but alas:
Not good! To remedy this, we will need to find a way to block the user from accessing certain content unless they are authenticated and that, even if they hack their way into accessing the content, there is no useful data for them to find anywhere.
The first step in adding basic security to our app is to create an authentication service, which can be used in order to carry out authentication requests. We want this service to provide the following functionalities:
Let's go ahead and build such a service. Add the services.js
file in the www/js
folder and insert the following content in it:
angular.module('secureApp.services', []) .factory('AuthFactory', function ($scope, $timeout) { var currentUser = null; var login = function (username, password) { return null; }; var isAuthenticated = function () { return false; }; var getCurrent = function () { return isAuthenticated() ? currentUser : null; }; return { login: login, isAuthenticated: isAuthenticated, getCurrent: getCurrent } });
This gives us a skeleton to work with. Let's start adding some meat to it incrementally.
The purpose of the login function is simply to take a username and password and check them against an existing list of such pairs. To get it working, we will first need to add some mock data to our service (in real life, you will of course pull the data from a remote server).
Go ahead and make sure that the LoginFactory
contains the following:
var validUsers = [ { firstName: 'Johanna', lastName: 'Doe', username: 'johnny', password: 'suchsecret' }, { firstName: 'Jane', lastName: 'Doe', username: 'zo1337', password: 'muchhide' }, { firstName: 'Mary', lastName: 'Doe', username: 'bl00dy', password: 'wow' } ];
Now, we simply need to add the following to the body of the login function:
var login = function (username, password) { var deferred = $q.defer(); // We use timeout in order to simulate a roundtrip to a server, // which will be present in any realistic authentication scenario. $timeout(function () { // Clear any existing, cached user data before logging in currentUser = null; // See if we can find a matching username-password match validUsers.forEach(function (user) { if (user.username === username && user.password === password) { // If we have a match, cache it as the current user currentUser = user; deferred.resolve(); } }); // If no match could be found, reject the promise if (!currentUser) { deferred.reject(); } }, 1000); // Return the promise to the caller return deferred.promise; };
What we do here in terms of authentication is really quite simple. We only match usernames
and passwords
against a pre-defined array. If a match is found, we cache the matched user and add it to the factories
context. It will now be accessible via the getCurrent()
function.
The purpose of this function is to allow the system to check whether the current user is presently logged in or not. We can simply implement it in terms of whether there is a cached user from a successful login event available:
var isAuthenticated = function () { return currentUser ? true : false; };
Now that we have a working authentication service, let's use it in order to safeguard the world's dolphins and seal off the private part of our app. To do so, first make sure that the index.html
file correctly imports the new service, as follows:
<script src="js/services.js"></script>
Next, modify the app.js
file to import that file as well:
angular.module('secureApp', [ 'ionic', 'secureApp.services', ])
Now, in the app.js
file, modify the routing config
for the private part of the app so that it looks like the following code:
.state('app.private', { url: "/private", views: { 'menuContent': { templateUrl: "templates/private.html", resolve: { isAuthenticated: function ($q, AuthFactory) { if (AuthFactory.isAuthenticated()) { return $q.when(); } else { $timeout(function () { $state.go('app.home') },0); return $q.reject() } } } });
What is going on here? To answer this, consider what we want to achieve. If the user is not authenticated, we want to send them back to the home screen until they have logged in. In order to do so, we perform the following steps:
app.private
state. In terms of the router, this is a function that has to be resolved before the navigation commences.AuthFactory.isAuthenticated
function that we defined earlier. However, for resolve
to work as expected, the return value of the hook needs to be a promise
method. Thus, we use $q
to return a when resolution if the user is logged in and a reject event if they are not.$state
in order to tell the router to redirect the control to the home page again.Finally, all we need to do is add an actual login screen for the app. To do so, start by adding a new file to keep controllers
for our app at the path www/js/controllers.js
. Make sure that this file has the following content:
angular.module('secureApp.controllers', ['secureApp.services']) .controller('AppCtrl', function ($scope, $ionicModal, $timeout, AuthFactory) { // Form data for the login modal $scope.loginData = {}; // Create the login modal that we will use later $ionicModal.fromTemplateUrl('templates/login.html', { scope: $scope }).then(function (modal) { $scope.modal = modal; }); // Triggered in the login modal to close it $scope.closeLogin = function () { $scope.modal.hide(); }; // Open the login modal $scope.login = function () { $scope.modal.show(); }; // Perform the login action when the user submits the login form $scope.doLogin = function () { AuthFactory.login($scope.loginData.username, $scope.loginData.password) .then(function () { $scope.closeLogin(); }); }; });
To render the login screen itself, add a template for to the path www/templates/login.html
:
<ion-modal-view> <ion-header-bar> <h1 class="title">Login</h1> <div class="buttons"> <button class="button button-clear" ng-click="closeLogin()"> Close </button> </div> </ion-header-bar> <ion-content> <form ng-submit="doLogin()"> <div class="list"> <label class="item item-input"> <span class="input-label"> Username </span> <input type="text" ng-model="loginData.username"> </label> <label class="item item-input"> <span class="input-label"> Password </span> <input type="password" ng-model="loginData.password"> </label> <label class="item"> <button class="button button-block button-positive" type="submit"> Log in </button> </label> </div> </form> </ion-content> </ion-modal-view>
Finally, let's tie everything together by making sure that the app loads our newly defined controller. Load it in index.html
:
<script src="js/controllers.js"></script>
Next, make sure that it is listed as a dependency in app.js
:
angular.module('secureApp', [ 'ionic', 'secureApp.services', 'secureApp.controllers' ])
We are now building our app. You can try it out by running it yourself. Try logging in with wrong credentials (according to the ones that we defined) in order to convince yourself that the app really blocks the user from going where they should not.
3.144.41.229