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.