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.