Create an OTP login component

2020-10-14 08:08:00.0
Developers

Hello guys, by default, Kademi provided normal login form with username and password. But you may need a "One Time Password" (OTP) login form in some specific cases. In this tutorial, we will learn how to create an OTP Login component. Now, let's learn how to create an OTP login form with Kademi.

First, we use App builder to create new application called "Sample App". Inside that app we'll create a component called "loginOtp". This app will need to include client side javascript so we'll also need dependencies file

So we'll we create these files:

- APP-INF/controllers.xml

- APP-INF/app.js

- website/theme/apps/sampleapp/components/loginOtpComponent.html

- website/theme/apps/sampleapp/dependencies.json

- website/theme/apps/sampleapp/scripts.js

 

 

Step 1: controllers.xml


    /APP-INF/app.js
    
    
    
       

Step 2: app.js

In the app.js, we need to define the loginOtp component, it will let the component is available for users.

// Define loginOtp component 
controllerMappings.addComponent("sampleapp/components", "loginOtp", "html", "Use this for login OTP", "sampleapp"); 

Because, we are creating an OTP login component, so we need to have 2 steps:
1. User enter the phone number, system generate OTP and send SMS to user. We need a controller call getOtp to handle this.
2. User received OTP and enter it into OTP verification form to login into Kademi system. We need another controller call loginOtp to handle this.


Ok, now we will define the controller which has url /samples/getOtp and handle creating and sending OTP sms:

controllerMappings
        .websiteController()
        .enabled(true)
        .isPublic(true)
        .path('/samples/getOtp')
        .addMethod('POST', 'getOtp')
        .postPriviledge("READ_CONTENT")
        .build();

Of course, we need a function name getOtp as defined to process creating and sending OTP sms. 

Note that, we have 3 main APIs are used in getOtp controller:

findMatchingProfiles: This API is under UserManager, help us find user profiles which are match with our provided request. To use this, you need combine with newProfileMatchRequest API of UserManager. See code bellow for example.

generatePasswordReset: This API is under UserManager, we can use it to generate the reset password token, the inputs are Profile and Website object. In this case, we use reset password token as One Time Password (OTP).

smsManager: SmsManager provides all APIs which handle SMS issues. In this case, we use it to send SMS. Remember to install KSms and Clicksend App and config Clicksend app in Settings section to send real SMS. Check Clicksend integration documentation here: Clicksend integration

function getOtp(page, params, files, form) {
    // Get phone from the login form
    var phone = form.cleanedParam('phone');
    // Get current website
    var wrf = page.find("/");
    var website = wrf.website;
    // Start userManager service and get profiles which is match with the phone
    var um = services.userManager;
    var pmr = um.newProfileMatchRequest().phone(phone);
    var profs = um.findMatchingProfiles(pmr);
    profile = profs.get(0);
    
    transactionManager.runInTransaction(function () {
        // Generate password otp using generatePasswordReset
        var passwordOtp = um.generatePasswordReset(profile, website);
        
        var message = "Your login OTP is: " + passwordOtp.token;
        
        var smsItem = services.smsManager.send(profile, message);
    });
    
    jsonResult = views.jsonResult(true);
    return jsonResult;
}

Next, we need to build the second controller loginOtp, which has url samples/loginOtp and handle verifying and logging user into Kademi system.

controllerMappings
        .websiteController()
        .enabled(true)
        .isPublic(true)
        .path('/samples/loginOtp')
        .addMethod('POST', 'loginOtp')
        .postPriviledge("READ_CONTENT")
        .build();

Like the first controller, we need a function to handle loginOtp:

Inside loginOtp controller, we used 2 main APIs:

findPasswordReset: This API is under UserManager and will return PasswordReset object which included user profile. This API need 2 params: reset pasword token - in this case we called it OTP and Website object

securityManager.authenticate: This API is under SecurityManager and we use it to login an user to Kademi system with Profile's name.

function loginOtp(page, params, files, form) {
    // Get otp from user form.
    var otp = form.cleanedParam('otp');
    
    // Get current website
    var wrf = page.find("/");
    var website = wrf.website;
    
    // Get do reset password page via otp
    var passwordReset = services.userManager.findPasswordReset(otp, website);
    
    // Do Logging passwordReset
    log.info('PASSWORDRESET: {}', passwordReset);
    
    // Check if token is not available
    if(formatter.isNull(passwordReset)){
        log.info()
        return views.jsonResult(false, 'OTP is not found');
    }
    // Check if token is used
    if(!formatter.isNull(passwordReset.usedDate)) {
        return views.jsonResult(false, 'OTP is used');
    }
    // Check if token is expired
    var expiredDate = formatter.addDays(passwordReset.createdDate, 30);
    if(formatter.between(expiredDate, null, formatter.now)) {
        return views.jsonResult(false, 'OTP is expired');
    }
    // Get user profile which is match with otp
    var profile = passwordReset.profile;
    
    // Do login
    transactionManager.runInTransaction(function () {
        services.securityManager.authenticate(profile.name);
    });
        
    jsonResult = views.jsonResult(true, 'Login success!');
    return jsonResult;
}

Let's combine all these code blocks above and we will have final app.js as bellow:

// Define loginOtp component
controllerMappings.addComponent("sampleapp/components", "loginOtp", "html", "Use this for login OTP", "sampleapp");

// Define getOtp controller, which will send OTP SMS
controllerMappings
        .websiteController()
        .enabled(true)
        .isPublic(true)
        .path('/samples/getOtp')
        .addMethod('POST', 'getOtp')
        .postPriviledge("READ_CONTENT")
        .build();

// Define loginOtp controller, which handle verify OTP and do user login
controllerMappings
        .websiteController()
        .enabled(true)
        .isPublic(true)
        .path('/samples/loginOtp')
        .addMethod('POST', 'loginOtp')
        .postPriviledge("READ_CONTENT")
        .build();

function getOtp(page, params, files, form) {
    // Get phone from the login form
    var phone = form.cleanedParam('phone');
    // Get current website
    var wrf = page.find("/");
    var website = wrf.website;
    // Start userManager service and get profiles which is match with the phone
    var um = services.userManager;
    var pmr = um.newProfileMatchRequest().phone(phone);
    var profs = um.findMatchingProfiles(pmr);
    profile = profs.get(0);
    
    transactionManager.runInTransaction(function () {
        // Generate password otp using generatePasswordReset
        var passwordOtp = um.generatePasswordReset(profile, website);
        
        var message = "Your login OTP is: " + passwordOtp.token;
        
        var smsItem = services.smsManager.send(profile, message);
    });
    
    jsonResult = views.jsonResult(true);
    return jsonResult;
}

function loginOtp(page, params, files, form) {
    // Get otp from user form.
    var otp = form.cleanedParam('otp');
    
    // Get current website
    var wrf = page.find("/");
    var website = wrf.website;
    
    // Get do reset password page via otp
    var passwordReset = services.userManager.findPasswordReset(otp, website);
    
    // Do Logging passwordReset
    log.info('PASSWORDRESET: {}', passwordReset);
    
    // Check if token is not available
    if(formatter.isNull(passwordReset)){
        log.info()
        return views.jsonResult(false, 'OTP is not found');
    }
    // Check if token is used
    if(!formatter.isNull(passwordReset.usedDate)) {
        return views.jsonResult(false, 'OTP is used');
    }
    // Check if token is expired
    var expiredDate = formatter.addDays(passwordReset.createdDate, 30);
    if(formatter.between(expiredDate, null, formatter.now)) {
        return views.jsonResult(false, 'OTP is expired');
    }
    // Get user profile which is match with otp
    var profile = passwordReset.profile;
    
    // Do login
    transactionManager.runInTransaction(function () {
        services.securityManager.authenticate(profile.name);
    });
        
    jsonResult = views.jsonResult(true, 'Login success!');
    return jsonResult;
}

       

Step 3: loginOtpComponent.html

#set ( $fixedCountry = $formatter.toString($fixedCountry)) 
#if ($formatter.isNull($fixedCountry)) 
    #set ($phoneify = $applications.get("phoneify-lib")) 
    #set ($rf = $formatter.ifNull($page, $rootFolder)) 
    #set ($defaultCountry = $phoneify.call("getAppSettings",$rf).defaultCountry) 
    #if ($defaultCountry) 
        #set ($fixedCountry = $defaultCountry) 
    #end 
#end    

   
           
   

 

Step 4: dependencies.json

     {
    "dependencies": [
        
        {
            "js": {
                "group": "main",
                "path": "/theme/apps/sampleapp/scripts.js"
            }
        }
    ]
} 

Step 5: scripts.js

    $(function() {
        if ($('.panel-get-otp').length > 0) {
            $('.panel-get-otp form').forms({
                requiredErrorMessage: "Please enter a valid phone address below.",
                onSuccess: function(resp) {
                    $('.panel-get-otp').hide();
                    $('.panel-login-otp').show();
                }
            });
        }
        
        if ($('.panel-login-otp').length > 0) {
            $('.panel-login-otp form').forms({
                requiredErrorMessage: "Please enter your OTP below.",
                onSuccess: function(resp) {
                    window.location.href = '/dashboard';
                }
            });
        }
    });   

After finish all these steps above, you should see the component as image bellow

Next, after add loginOtp component to your target page, you should see it display as bellow:

After you enter correct phone number, you should see the otp input as bellow:

undefined

Now you can enjoy your loginOtp component. See you guys on the next tutorials.