Our second article in this series is about Two Factor Authentication. Two Factor Authentication or Multi Factor Authentication (2FA or MFA) require the user to supply multiple forms of authentication something the user ‘knows’ and something the user is in possession of or something the user is.

For example an ATM withdrawal consists of the passcode (something the user knows) and the ATM cing the user possesses).ard (someth

In case of a login the simplest form of 2FA would be the password (something the user knows) and a code sent to the users email or phone (something the user possesses).

In a multi factor authentication other options come into play such as bio metrics (the users fingerprint, face id, voice id, etc).

We are going to look into adding simple two factor authentication to a DataFlex WebApp.

A standard DataFlex WebApp currently has a simple login screen that authenticates the user and creates an active session. The system then allows the user to use the application.

In order to support 2FA we will need to modify the login logic. In a 2FA system we will first validate the first part of the authentication which is the password.

Once validated we will need to create a random token and communicate that token to the user and the user will then need to enter that token to actually gain access to the application

First lets dive a bit deeper into how the login works in a DataFlex WebApp

The UserLogin method of the session handler validates the username and password combination and then updates a record in the WebAppSession table to reflect the login.

Our first step is to add a second screen for the confirmation of the Two Factor Authentication code.  This screen is very similar to the login screen.

We also need to create a code and send the code to the user. Because of the way web applications work we cannot simply store this in memory. The data in memory would not be available on the next call from the client

To handle the 2FA codes we will create a database table as follows

Name: WEBAPP2FA
Columns: SESSIONKEY ASCII 36, CREATEDDATE DATE, CREATEDTIME ASCII 8, CODE ASCII 20
Index 1: SESSIONKEY, CREATEDDATE, CREATEDTIME

now we also need a method to create and send the code. We will add the following method to the oWebApp object

//
Function RequestSendTwoFactorAuthCode String sToEmail Returns Boolean
    Open WebAppSession
    Open WEBAPP2FA
        
    // fail if we do not have a session
    If not status WebAppSession Function_Return (False)
                
    DateTime dtCurrentDateTime        
    Move (CurrentDateTime()) to dtCurrentDateTime
    
    Clear WEBAPP2FA
    Move WebAppSession.SessionKey       to WEBAPP2FA.SessionKey
    Get TimeToString dtCurrentDateTime  to WEBAPP2FA.CreateTime
    Move dtCurrentDateTime              to WEBAPP2FA.CreateDate
        
    Integer iRand        
    Move (random(999998)+1) to iRand
    Move (Trim(iRand)) to WEBAPP2FA.CODE
        
    SaveRecord WEBAPP2FA
                        
    Send DoSendTwoFactorAuthEmail sToEmail WEBAPP2FA.CODE
                                         
    Function_Return (True)
End_Function

The RequestSendTwoFactorAuthCode method creates a simple random code and then sends the code via email to the user.
The code to actually send the email is left out. Simply use whatever method you are using to send email. You can also send text messages or use any other method to send the code to the end user.
For testing purposes of course you can simply get the code from the database table as well.

We can now modify the DoLogin method in the login view to call this function and then show the TwoFactorAuthentication view.

//    
Procedure DoLogin
    String sLoginName sPassword
    Boolean bResult
    Handle hoDefaultView
        
    WebGet psValue of oLoginName to sLoginName
    WebGet psValue of oPassword to sPassword
        
    Get UserLogin of ghoWebSessionManager sLoginName sPassword to bResult
        
    If (bResult) Begin
        // login successful
        Send Hide of oLoginDialog

        // clear the login values. we don't want to return the login id & password as synchronized properties....
        WebSet psValue of oLoginName to ""
        WebSet psValue of oPassword  to ""
        WebSet pbVisible of oWarning to False

        // now send and validate 2FA code
        String sEmail

        // get logged in users email here
                    
        Boolean bOk
        Get RequestSendTwoFactorAuthCode of oWebApp sEmail to bOk
        If (bOk) Begin
            // show Two Factor Auth View
            Send Show to oTwoFactorAuthView               
        End
        Else Begin                
            WebSet pbVisible of oWarning to True
        End
    End
    Else Begin
        WebSet pbVisible of oWarning to True
    End
End_Procedure

The new login method works a bit different now. It first validates the login credentials.
Then instead of showing the default view we are sending a two factor authentication code to the user and redirect the user to the Two Factor Authentication validation page.

The user will now have to wait to receive the Two Factor Authentication code to enter it and proceed with the login.

But … here is a problem. If we enter the credentials and then when the Two Factor Authentication page loads we will simply go back to the apps main url DataFlex WebApp thinks we are logged in already and simply skip the Two Factor Authentication.

That is obviously not what we want. We need to teach DataFlex WebApp about the different statuses of a session

  • Session Login Validated but Two Factor Authentication missing
  • Session Login Validated and Two Factor Authentication validated

to handle this we will add a field to the WebAppSession table. The fields name is MFASTATUS and it is a single character ASCII field

After a successful login and sending of the Two Factor Authentication key we will set the value of this field to ‘W’ for Waiting Authentication.

Once the user authenticates with the Two Factor Authentication code we will clear the value in this field. This will then allow us to prevent the user from skipping the Two Factor Authentication

The design of the login code in DataFlex WebApp is not very developer friendly and doesnt provide hooks in areas were they would allow us to add functionality like this.

Because of this we have to add the following code to the SessionManager object in the WebApplication which is a copy of DAWs original login code plus the additional code to initialize the database fields for Two Factor Authentication

//
Function UserLogin String sLoginName String sPassword Returns Boolean
    String sSessionKey sUserPassword
    Handle hoSessionDD hoUserDD
    Boolean bMatch  
        
    Get phoSessionDD to hoSessionDD
    Get phoUserDD to hoUserDD
    Integer iErr eLoginMode
        
    // Refind session record
    Get psSessionKey to sSessionKey
    Send Clear of hoSessionDD
    Move sSessionKey to WebAppSession.SessionKey
    Send Find of hoSessionDD EQ Index.1
        
    If (Found and WebAppSession.SessionKey = sSessionKey) Begin
        Get peLoginMode to eLoginMode
            
        //  Find the user
        Move sLoginName to WebAppUser.LoginName
        Send Find of hoUserDD EQ Index.1
            
        // Check username and password
        If (Found and (Lowercase(sLoginName) = Lowercase(Trim(WebAppUser.LoginName)))) Begin
            Get Field_Current_Value of hoUserDD Field WebAppUser.Password to sUserPassword
            Get ComparePasswords (Trim(sUserPassword)) (Trim(sPassword)) to bMatch
               
            If (bMatch) Begin
                // Store the login
                Set Field_Changed_Value of hoUserDD Field WebAppUser.LastLogin to (CurrentDateTime())
                Get Request_Validate of hoSessionDD to iErr
                If (iErr) Begin
                    // this should not happen. If it does its a programming error
                    Error DFERR_PROGRAM C_$WebAppSesionValidateFailed
                    Function_Return False
                End
                    
                // Require Two Factor Authentication
                Set Field_Changed_Value of hoSessionDD Field WebAppSession.MFASTATUS to "W"
                    
                Send Request_Save of hoSessionDD
                    
                // Update session properties
                Send UpdateSessionProperties False
                Send NotifyChangeRights
                Function_Return True
            End
            Else Begin
                //  We should rely directly on this buffer elsewhere but just be sure
                Send Clear of hoUserDD
            End
        End
    End
       
    Function_Return False
End_Function

the code above will handle the login and if successful set the WebAppSessions MFASTATUS field to ‘W’.

We will also need to modify the LoadWebApp method to ensure it will send the user to the Two Factor Authentication page when needed.

Function LoadWebApp Boolean bOptLoadInitial String sClientLanguage Integer iClientTimezoneOffset Returns String
    String sStartupView sStateHash
    Integer eLoginMode
    Boolean bIsLoggedIn bAllowAccess bOverrideState bLoadInitial
    tWebObjectDef[] aObjectDefs
    tWebObjectDef ObjectDef
    Handle hoView hoDefaultView
    Handle[] aDDOViews
    tWebNameValue[] aTranslations
    tWebValueTree tData
    String[] aParams
    
    //  bLoadInitial is optional to maintain compatibility with 18.1, we don't want to throw errors before the version check
    If (num_arguments >= 1) Begin
        Move bOptLoadInitial to bLoadInitial
    End
    Else Begin
        Move True to bLoadInitial
    End
    
    //  Trigger event allowing application to initialize things based on the client locales
    Send OnInitializeLocales sClientLanguage iClientTimezoneOffset
    
    //  Load translations
    Get ClientTranslations to aTranslations
    ValueTreeSerializeParameter aTranslations to tData
    Send ClientAction "initTranslations" aParams tData
    
    //  Serialize Global Objects
    Get SerializeObject to ObjectDef
    Get paObjectDef to aObjectDefs
    Move ObjectDef to aObjectDefs[SizeOfArray(aObjectDefs)]
    Set paObjectDef to aObjectDefs
    
    //  Determine if loginview needs to be shown
    Get peLoginMode to eLoginMode
    
    If (eLoginMode=lmLoginRequired) Begin
        // Test for login....
        Get IsLoggedIn of ghoWebSessionManager to bIsLoggedIn
        
        If (not(bIsLoggedIn)) Begin
            // Send back the login view as the first view to load
            If (bLoadInitial) Begin
                Get GetLoginView to hoView
            End
            // If hoView is 0, this is probably a progamming error. No way to log in and it is required.
            // For now do nothing which will load the default view
        End
    End

    // reroute user to Two Factor Auth Page when needed
    Declare_Datafile WebAppSession        
    If (WebAppSession.MFASTATUS="W") Begin
        Move (oTwofactorAuthView(Self)) to hoView
    End        

    //  Set initial page title (a login dialog might not set one)
    Send UpdatePageTitle C_WebUnresolvedObject ""
    
    //  Send back default view unless a login dialog needs to be sent
    If (hoView = 0) Begin
        //  Only load the initial view when needed
        If (bLoadInitial and SizeOfArray(aObjectDefs) <= 1) Begin
            //  Restore application state
            If (peApplicationStateMode(Self) <> asmOff) Begin
                Get StateHash to sStateHash 
                Send RestoreState sStateHash True
            End
            Else Begin //  Or return default view
                Get GetDefaultView to hoView
                If (hoView > 0) Begin
                    Send Show of hoView
                End
            End
        End
    End
    Else Begin
        Send Show of hoView

    End
     
    Function_Return ""
End_Function

In the Two Factor Authentication view we will allow the user to type in a code and then verify it against our database

Procedure DoConfirmCode
    DateTime dtCurrentDateTime
    
    Move (CurrentDateTime()) to dtCurrentDateTime

    Clear WEBAPP2FA
    Move WebAppSession.SessionKey to WEBAPP2FA.SESSIONKEY
    String sTimeNow
    Get TimeToString of oWebApp dtCurrentDateTime to sTimeNow
    Move dtCurrentDateTime to WEBAPP2FA.CREATEDATE
    Move sTimeNow to WEBAPP2FA.CreateTime
    Find LE WEBAPP2FA by Index.1
    If (WEBAPP2FA.SESSIONKEY<>WebAppSession.SessionKey) Move False to Found
    If (WEBAPP2FA.CREATEDATE>Date(dtCurrentDateTime)) Move False to Found
    If (WEBAPP2FA.CREATETIME>sTimeNow) Move False to Found
    If (not(Found)) Begin
        Send ShowInfoBox "Invalid Code"
        Procedure_Return
    End
    
    // check expiration on code
    
    String sCode
    WebGet psValue of oValidationCode to sCode
    If (trim(WEBAPP2FA.CODE)<>Trim(sCode)) Begin
        Send ShowInfoBox "Code is invalid"
        Procedure_Return            
    End
        
    Reread WebAppSession
    Move "" to WebAppSession.MFASTATUS
    SaveRecord WebAppSession
    Unlock
    
    Integer hoDefaultView
    Send ShowHeader of ghoWebApp 
    WebSet psCSSClass of ghoWebApp to ""
    Get GetDefaultView to hoDefaultView
    If (hoDefaultView > 0) Begin
        Send Show of hoDefaultView
    End        
        
End_Procedure

Our DoConfirmCode procedure, called from the buttons OnClick event does just that. First we find the last 2FA code in our database. If we  cannot find an entry we bail out with an error message.

If we can find an entry we validate the code. At this time this code simply uses the last code sent to the user. A better implementation will use expiration time and also allow multiple active codes within the expiration and then also remove any entries that are expired to clean up the database.

This will now allow us to run the application which will bring up the login page. Once a login is successful the system will generate a Two Factor Authentication code and send the user to the validation screen. Once the proper code is entered the application will start as it should.

But there is another problem. If the user, after logging in successfully uses a direct url to navigate to one of the views of the application the system will allow him to do that without having to enter the 2FA code.

We can fix this by overriding the IsLoggedIn event in the session manager obect

Function IsLoggedIn Returns Boolean
    Boolean bLoggedIn
    
    Forward Get IsLoggedIn to bLoggedIn
    
    If (bLoggedIn) Begin
        If (WebAppSession.MFASTATUS="W") Begin
            Function_Return (False)            
        End
    End

    Function_Return bLoggedIn
End_Function

This will now prevent a user from navigating to any page directly before the Two Factor authentication is complete

As mentioned already this is a base implementation and in a real world system would have a few more features such as expiration time on the 2FA code but the main part of this post is to show how to add functionality like this to an existing DataFlex WebApp .

The Two Factor Authentication can also be combined with the reCaptcha functionality. In a real world system we would use reCaptcha V3 to get a human/robot score. if the score points to certainly human we can simply proceed with the login. If the score is certain robot we can show an error screen or reroute to a page intended for a robot. All scores in between would be forced to use Tow Factor Authentication to log into the system.

Michael Salzlechner is the CEO of StarZen Technologies, Inc.

He was part of the Windows Team at Data Access Worldwide that created the DataFlex for Windows Product before joining StarZen Technologies. StarZen Technologies provides consulting services as well as custom Application development and third party products specifically for DataFlex developers