Testing 2 Factor Authentication with Selenium
He who knows others is wise. He who knows himself is enlightened. 知人者智,自知者明。
, Lao Tzu (老子)
12 Jan 2018
Introduction
An earlier article shows how to build a 2 Factor authentication mechanism using Google Authenticator Mobile App. This article shows how to automate testing of the 2 factor authentication mechanism using Selenium WebDriver and Junit. Selenium is a browser automation tool offering an API to control and automate browser actions. It can be used with Junit to create automated test cases and test suites for web applications.
Design and Approach
The 2 Factor authentication application is available from Github at https://github.com/ngchianglin/2faAppEngineJava8. Download a copy and import into Eclipse IDE as a maven project. To enable Selenium, we can add the relevant dependency to the pom.xml. In this case, we are using version 3.6.0 of the selenium-java package. The dependency for Junit version 4.12 should already be in the pom.xml.
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.6.0</version>
<scope>test</scope>
</dependency>
The Junit and Selenium test cases can be created under the src/test/java directory of the project. Before we create the automated tests, we need to set up the 2 Factor Authentication Application and deploy it to Google App engine. Refer to the previous article, Implementing 2 Factor Authentication for Web Security, for details on how to do this.
Once the 2FA application has been setup and running. We can create a java class, TestConstants to hold constants such as test credentials, OTP secret key that the other test cases will use. Creating a single class to store these sensitive data make it easier to manage the testing.
We will use Mozilla firefox for the automated testing. Selenium WebDriver version 3, requires a native driver, Mozilla GeckoDriver to be installed. Refer to https://www.seleniumhq.org/download/ for the links to download these third party native drivers. Download and unzip the GeckoDriver binary to a local directory.
For the OTP functionality, the test case will require the sg.nighthour.crypto.TimeBaseOTP and CryptoUtil classes. This class is already part of the 2 FA application. If you encounter class not found errors when running the automated test case, make sure that these two classes are in the classpath.
Implementation
The full source code of the automated tests are available in the same Github link for the 2 FA application. They are located under the src/test/java directory. We will run through the code here.
TestConstants.java define the constants and credentials used for the testing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /*
* MIT License
*
*Copyright (c) 2018 Ng Chiang Lin
*
*Permission is hereby granted, free of charge, to any person obtaining a copy
*of this software and associated documentation files (the "Software"), to deal
*in the Software without restriction, including without limitation the rights
*to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
*copies of the Software, and to permit persons to whom the Software is
*furnished to do so, subject to the following conditions:
*
*The above copyright notice and this permission notice shall be included in all
*copies or substantial portions of the Software.
*
*THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
*IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
*FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
*AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
*LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
*OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*SOFTWARE.
*
*/
public class TestConstants
{
//Defines the Gecko Webdriver location for firefox
protected static final String GECKODRIVER_PATH = "E:\\geckodriver-v0.19.0-win64\\geckodriver.exe";
//Defines the firefox gecko driver
protected static final String GECKODRIVER = "webdriver.gecko.driver";
//The sleep interval
protected static final long SLEEP_INTERVAL = 2000;
//Test credentials
protected static final String TESTUSER="user1@nighthour.sg";
protected static final String TESTPASSWORD = "XXXXXXXXXXXXXXXXXXXXXXXXXXX";
//Test OTP secret key in hexadecimal
protected static final String TESTOTPSECRET="XXXXXXXXXXXXXXXXXXXXXXXXXXXX";
//Login url
protected static final String LOGIN = "https://single-tide-189607.appspot.com/index.jsp";
//The expected fullname of the user
protected static final String FULLNAME = "Tan WeiMing";
}
|
Replace the constants defined here with those relevant for your setup. The TESTUSER variable holds the userid of the account, TESTPASSWORD variable holds the account password and TESTOTPSECRET is the hexadecimal string of the OTP secret key. This OTP secret key is what is configured in the user entity stored in the Google cloud datastore. Refer to the earlier article Implementing 2 Factor Authentication for Web Security, for more information on setting up the 2FA application.
LoginTest.java contains the code for testing the 2FA login.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | /*
* MIT License
*
*Copyright (c) 2018 Ng Chiang Lin
*
*Permission is hereby granted, free of charge, to any person obtaining a copy
*of this software and associated documentation files (the "Software"), to deal
*in the Software without restriction, including without limitation the rights
*to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
*copies of the Software, and to permit persons to whom the Software is
*furnished to do so, subject to the following conditions:
*
*The above copyright notice and this permission notice shall be included in all
*copies or substantial portions of the Software.
*
*THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
*IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
*FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
*AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
*LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
*OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
*SOFTWARE.
*
*/
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import sg.nighthour.crypto.TimeBaseOTP;
import sg.nighthour.crypto.CryptoUtil;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
public class LoginTest
{
private String GECKODRIVER_PATH = TestConstants.GECKODRIVER_PATH;
private String GECKODRIVER = TestConstants.GECKODRIVER;
private String userid= TestConstants.TESTUSER;
private String password= TestConstants.TESTPASSWORD;
private String url = TestConstants.LOGIN;
private String fullname = TestConstants.FULLNAME;
@Before
public void setUp() throws Exception
{
System.setProperty(GECKODRIVER,GECKODRIVER_PATH);
}
@Test(timeout=60000)
public void test() throws InterruptedException
{
WebDriver driver = new FirefoxDriver();
driver.get(url);
Thread.sleep(TestConstants.SLEEP_INTERVAL);
driver.findElement(By.name("userid")).sendKeys(userid);
WebElement element = driver.findElement(By.name("password"));
element.sendKeys(password);
element.submit();
Thread.sleep(TestConstants.SLEEP_INTERVAL);
try
{
String otpresult = TimeBaseOTP.generateOTP(CryptoUtil.hexStringToByteArray(TestConstants.TESTOTPSECRET));
WebElement otpelement = driver.findElement(By.name("totp"));
otpelement.sendKeys(otpresult);
otpelement.submit();
Thread.sleep(TestConstants.SLEEP_INTERVAL);
WebElement displayuser = driver.findElement(By.tagName("p"));
String usertext = displayuser.getText();
assertTrue("Login failed !",usertext.contains(fullname));
}
catch(NoSuchElementException e)
{
fail("Login failed !");
}
finally
{
Thread.sleep(TestConstants.SLEEP_INTERVAL);
driver.close();
}
}
}
|
The Junit setUp() method, configures the path to the Mozilla GeckoDriver. We set a timeout of 60 seconds for the test() method through java annotation. Since we are testing a web application, setting a suitable timeout will ensure that the test eventually terminates.
In the test() method itself, Selenium WebDriver API is called to automate loading the login page, entering the userid and password. TimeBaseOTP.generateOTP() method is called to generate the OTP token. This token is then submitted. The code then tests whether the user fullname is displayed by the application. A suitable sleep interval of 2 seconds are set in-between these steps, to ensure that the each web page loads fully before proceeding.
Running the Automated Junit and Selenium Test
In Eclipse IDE, select LoginTest.java under src/test/java, right click and select "Run As" >> "Junit Test".
The firefox browser should be started up automatically and the login page loaded.
The test code will then fill in and submit the userid and password. It then waits 2 second for the OTP form to come up and submits the OTP token. Note the time on the computer has to be accurate as the OTP token is time-based. The following screenshot shows selenium filling in the OTP.
After submitting the OTP, the code checks that the fullname of the user is displayed. If there is an error logging in, it will exit with an error.
The Junit tab in Eclipse will show the results of the test run. The green color indicates that the test has completed successfully.
Conclusion and Afterthought
Testing is an important aspect of building secure and bug free applications. Automating end-to-end testing using Selenium WebDriver can improve the reliability of an application, preventing regressions and speeding up development and deployment.
Automated testing has an important role in security regardless of whether it is traditional physical infrastructure or a modern cloud architecture. It speeds up development and deployment, offering some assurances that the application or infrastructure is performing as it should.
The full source code for the 2FA application and the automated Junit/Selenium test is available at the following Github link.
https://github.com/ngchianglin/2faAppEngineJava8
If you have any feedback, comments, corrections or suggestions to improve this article. You can reach me via the contact/feedback link at the bottom of the page.
Article last updated on Feb 2018.