Ionic 4 Two-factor Authentication. Part 1

Introduction

In this tutorial, we will show how to set up two-factor authentication using Twilio and a user registration feature with phone binding to your Appery.io app. Here's how it works: when a user signs up, the user's mobile phone is used to verify the user's identity by entering the number sent by the authenticator app.

Twilio Account Setup

  1. Go to https://www.twilio.com/try-twilio and create a new account.
  2. Login and go to the console.
  3. Confirm your email and telephone number.

📘

Twilio setup tip

You can quickly go through the Twilio onboarding questionnaire or click Skip to dashboard instead.

  1. On the dashboard, click Get a Trial Number:
1917
  1. In the popup that appears you will see your first Twilio phone number. It will be used to send SMS messages to users who sign up to your app. Click Choose this Number and then click Done:
871
  1. Check that the phone number that you will use for testing has been added to the Active Numbers list:
1418
  1. Now, check your Verified Caller IDs tab: it should contain the verified number: this number will receive a notification code from your Twilio trial account.
  2. Make sure that sending SMS for the indicated region (in this case, for the US region) has been enabled in your Twilio console: Geo permissions for SMS section:
1911

Creating Twilio Server Code Library

  1. Log in to Appery.io and open the Server Code tab.

  2. Click Create library and enter twilioLibrary as a name. Copy the code below and paste it in the code editor:

var TWILIO_BASE_URL = 'https://api.twilio.com/2010-04-01',
    ACCOUNT_SID = '',
    SENDER_PHONE = '',
    AUTH_TOKEN = '';

(function(context) {

    var Twilio = {
        sendSMS: sendSMS
    };

    context.Twilio = Twilio;

    function sendSMS(message, to) {
        console.log(to);
        var response = XHR2.send('POST', TWILIO_BASE_URL + '/Accounts/' + ACCOUNT_SID + '/Messages', {
            'body': 'To=' + encodeURIComponent(to) + '&From=' + encodeURIComponent(SENDER_PHONE) + '&Body=' + encodeURIComponent(message),
            'headers': {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Authorization': 'Basic ' + encodeBase64(ACCOUNT_SID + ':' + AUTH_TOKEN)
            }
        });
        console.log(response);
        return response;
    }

    function encodeBase64(input) {
        var keyStr = 'ABCDEFGHIJKLMNOP' +
            'QRSTUVWXYZabcdef' +
            'ghijklmnopqrstuv' +
            'wxyz0123456789+/' +
            '=';
        var output = "";
        var chr1, chr2, chr3 = "";
        var enc1, enc2, enc3, enc4 = "";
        var i = 0;
        do {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
            output = output +
                keyStr.charAt(enc1) +
                keyStr.charAt(enc2) +
                keyStr.charAt(enc3) +
                keyStr.charAt(enc4);
            chr1 = chr2 = chr3 = "";
            enc1 = enc2 = enc3 = enc4 = "";
        } while (i < input.length);
        return output;
    }

})(this);
  1. Go back to the Twilio console. Copy ACCOUNT SID, AUTH TOKEN and the PHONE NUMBER values:
1451
  1. At the top of the previously created twilioLibrary script, replace the values of the ACCOUNT_SID, SENDER_PHONE and AUTH_TOKEN variables with the copied values. Save the server code and then scroll down to click the Test button to check for errors:
1863

Creating Appery.io Database

  1. Open the Databases tab and click Create new database.
  2. Enter authDB as the name of a new library and click Create:
1230
  1. You will see the predefined Users collection. We will use it to store information about registered users. Let's modify the collection a bit by adding one more column. Create a new phone column of String type:
1237
  1. Then, click Create new collection. Enter TemporaryStack as a new collection name and confirm creating:
1219
  1. This collection will be used as a temporary storage of verification codes. In the collection, add two new columns: phone and code, both of String type:
1238
  1. That's it, the database is ready. Let's also copy the database API key from the Settings tab, we will need it in the next steps:
1245

Creating Appery.io Registration Server Code

  1. In the Server code tab, click the Create Script button to create a new script.
  2. Enter twoFactorAuthentication as its name and remove all code in the code editor. Then, type this line of code: var DB_ID = 'YOUR_DB_API_KEY';. Set the copied database API key as the DB_ID constant value (replace YOUR_DB_API_KEY text inside single quotes with its value). Click Save:
1250
  1. Go to the script Dependencies tab and select twilioLibrary as a dependency:
1250
  1. Then let's go back to the Script tab and complete the server code:
var DB_ID = 'YOU_DB_API_KEY';

try {
    var requestBody = JSON.parse(request.requestBody);
    var username = requestBody.username;
    var password = requestBody.password;
    var phone = requestBody.phone;
    var code = requestBody.code;
    var result;

    if (!username) throw new Error('Username is required.');
    if (!password) throw new Error('Password is required.');
    if (!phone) throw new Error('Phone number is required.');
    phone = phone.replace(/[-, ]/g, '');

    // User verification step
    if (!code) {
        // Generate code
        result = generateCode(phone);
    } else {
        // Validate code
        result = validateCode(phone, code);
    }

    // User registration step
    if (typeof result === "boolean") {
        if (!result) throw new Error('Verification code is not valid.');

        // Register user
        result = registerUser(username, password, phone);
    }

    response.success(result, "application/json");
} catch (e) {
    response.error({
        status: 'error',
        message: e.message
    }, 403);
}

function generateCode(phone) {
    var code = Math.floor(100000 + Math.random() * 900000);
    var message = 'Your verification code is: ' + code;
    
    // Send the generated code using the Twilio service
    var smsResult = Twilio.sendSMS(message, phone);
    if (!!smsResult && smsResult.status === 201) {
        var stackResult = getFromStack(phone);

        if (stackResult && stackResult.length) {
            // Update phone number and code for user verification step
            return Collection.updateObject(DB_ID, "TemporaryStack", stackResult[0]._id, {
                "phone": phone,
                "code": code
            });
        } else {
            // Save phone number and code for user verification step
            return Collection.createObject(DB_ID, "TemporaryStack", {
                "phone": phone,
                "code": code
            });
        }
    } else {
        throw new Error('Please check the phone number format');
    }
}

function getFromStack(phone) {
    var params = {
        criteria: {
            "phone": {
                "$eq": phone
            }
        }
    };
    // Search for existing records in the temporary stack
    return Collection.query(DB_ID, "TemporaryStack", params);
}

function validateCode(phone, code) {
    var validationResult = false;
    var stackResult = getFromStack(phone);

    if (stackResult && stackResult.length && stackResult[0].code === code) {
        validationResult = true;
        cleanFromStack(stackResult[0]._id);
    }

    return validationResult;
}

function cleanFromStack(id) {
    Collection.deleteObject(DB_ID, "TemporaryStack", id);
}

function registerUser(username, password, phone) {
    var user = DatabaseUser.signUp(DB_ID, {
        "username": username,
        "password": password,
        "phone": phone
    });
    return user;
}
1249

Reviewing Created Code

📘

Code review

Let's take a look at the parts of this code to understand its logic.

Note, that this part of the tutorial is optional and is intended to help you understand what is happening in the code.

  1. The following code part, generateCode function, adds logic of generating a one-time code and sending this code via SMS. Also, when the code is sent, we will store it in our temporary stack for validation purposes in the following steps:
function generateCode(phone) {
    var code = Math.floor(100000 + Math.random() * 900000);
    var message = 'Your verification code is: ' + code;
    
    // Send the generated code using the Twilio service
    var smsResult = Twilio.sendSMS(message, phone);
    if (!!smsResult && smsResult.status === 201) {
        var stackResult = getFromStack(phone);

        if (stackResult && stackResult.length) {
            // Update phone number and code for user verification step
            return Collection.updateObject(DB_ID, "TemporaryStack", stackResult[0]._id, {
                "phone": phone,
                "code": code
            });
        } else {
            // Save phone number and code for user verification step
            return Collection.createObject(DB_ID, "TemporaryStack", {
                "phone": phone,
                "code": code
            });
        }
    } else {
        throw new Error('Please check the phone number format');
    }
}
  1. The next additional function, getFromStack, supports the previous generateCode function and is used to search for existing verification codes in the temporary stack:
function getFromStack(phone) {
    var params = {
        criteria: {
            "phone": {
                "$eq": phone
            }
        }
    };
    // Search for existing records in the temporary stack
    return Collection.query(DB_ID, "TemporaryStack", params);
}
  1. The next function, validateCode, adds logic for validating the one-time code. If the code is valid, it cleans the record from the temporary stack:
function validateCode(phone, code) {
    var validationResult = false;
    var stackResult = getFromStack(phone);

    if (stackResult && stackResult.length && stackResult[0].code === code) {
        validationResult = true;
        cleanFromStack(stackResult[0]._id);
    }

    return validationResult;
}
  1. The next simple function, cleanFromStack, clears the record from temporary stack:
function cleanFromStack(id) {
    Collection.deleteObject(DB_ID, "TemporaryStack", id);
}
  1. The registerUser function adds user registration logic:
function registerUser(username, password, phone) {
    var user = DatabaseUser.signUp(DB_ID, {
        "username": username,
        "password": password,
        "phone": phone
    });
    return user;
}
  1. And this part is a "skeleton" of our server code script, that uses previously reviewed functions:
var DB_ID = 'YOUR_DB_API_KEY';

try {
    var requestBody = JSON.parse(request.requestBody);
    var username = requestBody.username;
    var password = requestBody.password;
    var phone = requestBody.phone;
    var code = requestBody.code;
    var result;

    if (!username) throw new Error('Username is required.');
    if (!password) throw new Error('Password is required.');
    if (!phone) throw new Error('Phone number is required.');
    phone = phone.replace(/[-, ]/g, '');

    // User verification step
    if (!code) {
        // Generate code
        result = generateCode(phone);
    } else {
        // Validate code
        result = validateCode(phone, code);
    }

    // User registration step
    if (typeof result === "boolean") {
        if (!result) throw new Error('Verification code is not valid.');

        // Register user
        result = registerUser(username, password, phone);
    }

    response.success(result, "application/json");
} catch (e) {
    response.error({
        status: 'error',
        message: e.message
    }, 403);
}

Creating Appery.io Mobile App

  1. On the Apps tab, click the Create new app green button to create a new project. Enter twoFactorAuthApp as the app name, select Ionic 4 project and Ionic 4 Blank type and click Create.
1041
  1. First, let’s create a storage that will contain the data required for the server code. Go to Project > Model and Storage. On the Storage tab, add a new storage variable named localData. Set its Type to Any:
1295
  1. From the left panel in the editor, click CREATE NEW > Server Code Service. Select the twoFactorAuthentication server code which we’ve created in the previous steps:
1417
  1. Unfold the Pages folder in the project tree on the left. Rename the default Screen1 page to SignUp by clicking the small cog icon next to the page name.
  2. Create one more page, for submitting the confirmation code. Click CREATE NEW > Page, enter CodeConfirmation as a new page name and confirm creating:
1548

Signup Page

  1. Open the SignUp page, go to its DATA tab, select the service you just imported and click Add. Change the service's generated local name to registrationService:
1543
  1. We will send the collected data to the server code with the use of mapping. Click the Mapping button in the Before send section. Map localData storage variable to the data parameter of the service request, as shown in the screenshot below. Click Save & Replace:
1224
  1. We won't need the Mapping in the Success section, so you can remove it. Then, click Add button in Success section. Select Navigate to page and set CodeConfirmation as the Route name:
1547
  1. Let's also create a custom popup for errors handling. Click the Add button in the Error section. For the action, select Run TypeScript and add the following code in the editor. Don't forget to save the changes:
const controller = this.Apperyio.getController('AlertController');
const alert = await controller.create({
    'header': 'Error',
    'message': err.error.message,
    'buttons': [{
        'text': 'OK',
    }]
});
return await alert.present();
  1. Switch to the CODE panel. In the Variables section create a new variable with formData name of Any type with {} default value:
994
  1. Then, go to the DESIGN panel. In the header, change the Text property of the Toolbar Title to Sign up.
  2. Drag & drop three Input components and a Button component to the page from PALETTE, one below the other:
627
  1. Set the following properties for the created components:
  • For the Input1 component: Placeholder = Enter your username, Label > Text = Username, [(ngModel)] = formData.username.

  • For the Input2 component: Placeholder = Enter your password, Type = Password, Label > Text = Password, [(ngModel)] = formData.password.

  • For the Input3 component: Placeholder = +xxx xx xxx xx xx, Type = Tel, Label > Text = Phone number, [(ngModel)] = formData.phone.

  • For the Button1 component: change its Text property to Sign up.

As a result, the page should look like this:

625
  1. Let’s add the logic to save form data to our storage. Select the Button1 component and expand the EVENTS tab from the bottom. For the Click event, set the Run TypeScript action. In the code editor, type the following code. Save the changes:
let localData = await this.Apperyio.data.getStorage("localData");

if (localData) {
    delete localData.code;
}

await this.Apperyio.data.setStorage("localData", {...localData,
    ...this.formData
});
1699
  1. Then, create one more Click event for the Button1 and select https://docs.appery.io/docs/ionic-4-events-and-actions#invoke-service. Select registrationService as the Datasource:
1560

Code Confirmation Page

  1. Switch to the CodeConfirmation page and open its DATA panel. In the dropdown, select the imported service and click Add. Change the service's generated local name to codeVerificationService:
1565
  1. Click the Mapping button in the Before send section. With the use of mapping we will send the collected data to the server code. Map localData storage variable to the data parameter of the service request, as shown in the screenshot below. Click Save & Replace:
1312
  1. We won't need the Success mapping here, so let's delete it. Then, click the Add button in the Success section and select Present alert. We can modify the alert popup parameters, let's set its Header to Success and its Message to You have been successfully registered:
1711
  1. Also, we will create our own custom popup for error handling. Click the Add button in the Error section. Select Run TypeScript and add the following code. Don't forget to save the changes:
let controller = this.Apperyio.getController('AlertController');
const alert = await controller.create({
    'header': 'Error',
    'message': err.error.message,
    'buttons': [{
        'text': 'OK',
    }]
});
return await alert.present();
  1. Now, switch to the CODE panel. In the Variables section, create a new variable with the code name and String type:
1089
  1. Navigate to the DESIGN panel. In the header, change the Text property of the Toolbar Title to Code confirmation.
  2. Drag & drop an Input component and a Button component to the page. Set the following properties for the created components:
  • For the Input1 component: Placeholder = Enter a one-time code from SMS, Label > Text = Code, [(ngModel)] = code.

  • For the Button1 component: change its Text property to Submit.

As a result, the page should look like this:

622
  1. Let’s add the logic of saving verification code from the form to our storage. Expand the EVENTS tab and select the Button1 component. Select Click as the Event and Run TypeScript as the Action. Add the following code, and don't forget to save the changes after editing:
const localData = await this.Apperyio.data.getStorage("localData");
await this.Apperyio.data.setStorage("localData", {...localData, code: this.code});
  1. Then, add one more Click event for the Button1 and select Invoke service for the Action and codeVerificationService as the Datasource:
1703

Save all app changes and that's it, we can test the app now.

App Testing

Click the TEST button in the top menu of the editor to test the application in the browser. Enter any username and password and your phone number and click Submit. The app will navigate to the code confirmation page, and you will receive an SMS with a verification code.

Check your mobile phone, enter a code from the SMS that was sent from your Twilio trial account:

945

Now, click Submit. If the code is valid, you will see a popup informing you that registration is complete:

1575