Night Hour

Reading under a cool night sky ... 宁静沉思的夜晚 ...

Testing 2 Factor Authentication with Selenium

Chinese Teapot Set

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.

Selenium GeckoDriver
Fig 1. Selenium GeckoDriver

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".

Run Junit Selenium Test - Eclipse
Fig 2. Run Junit Selenium Test - Eclipse

The firefox browser should be started up automatically and the login page loaded.

Run Junit Selenium Test - Firefox
Fig 3. Run Junit Selenium Test - Firefox

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.

Selenium autofills OTP
Fig 4. Selenium autofills 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.

Sucess page with user fullname
Fig 5. Sucess page with user fullname

The Junit tab in Eclipse will show the results of the test run. The green color indicates that the test has completed successfully.

Junit Results - Eclipse
Fig6. Junit Results - Eclipse

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.