Loading...

Client side certificate authentication in selenium tests

 Using client side certificate authentication is not very popular topic right now but I guess that it is still valid. It is also not very clear how to handle this scenario in the selenium tests. In this article I will explain how to create test framework which allows to authenticate with use of client side certificate authentication in Firefox browser.

First of all we need to create firefox profiles for all certificates we want test our application on. To create profile with desired certificate we need to open Firefox and import desired certificate (make sure that only one certificate is imported). When profile is created we need to copy cert9.db and key4.db files from our firefox profile folder to project resources (e.g. to /profiles/customer/ folder). Here you can find information about finding firefox profile's folder. You can repeat this section for every certificate you need in your tests.

The framework we will create now will be based on that from article Page objects, chaining and passing web driver. Lets assume that our application requires two factor authentication (client side certificate authentication and login with user credentials). The first class we create is UserType. This class defines what kind of user is logging in to application (e.g. admin, customer). Every type is connected to separate certificate and can have different rights in the application. As every type is assigned to different certificate we are passing in constructor the path in resources to firefox profile with this very certificate.

package com.testcraftsmanship.model.authentication;

public enum UserType {
    ADMIN("/profiles/admin/."), CUSTOMER("/profiles/customer/.");

    private final String profilePath;

    UserType(String profilePath) {
        this.profilePath = profilePath;
    }

    public String getProfilePath() {
        return profilePath;
    }
}


The next class is the User which contains all fields required to authenticate to the application.

package com.testcraftsmanship.model.authentication;

public class User {
    private UserType userType;
    private String userName;
    private String userPassword;

    public User(UserType userType, String userName, String userPassword) {
        this.userType = userType;
        this.userName = userName;
        this.userPassword = userPassword;
    }

    public UserType getUserType() {
        return userType;
    }

    public String getUserName() {
        return userName;
    }

    public String getUserPassword() {
        return userPassword;
    }
}


We do not need to update PageInitializer class so it stays as listed below:

package com.testcraftsmanship.model.base;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;

import java.lang.reflect.InvocationTargetException;

public interface PageInitializer {
    default <T extends BasePage> T newInstance(Class<T> clazz) {
        try {
            T page = clazz.getConstructor(WebDriver.class).newInstance(getDriver());
            PageFactory.initElements(getDriver(), page);
            return page;
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException("Error while creating new page instance", e);
        }
    }

    WebDriver getDriver();
}


DriverFactory class has to be changed a little bit because we need to pass the path to firefox profile while creating FirefoxDriver object. It is done by passing the path to cert9.db and key4.db files as an argument to FirefoxProfile constructor. We also have to set "security.default_personal_cert" preference to "Select Automatically" to avoid displaying dialog asking for certificate to select. FirefoxOptions created on this way have to be passed to FirefoxDriver constructor. Whole class is listed below:

package com.testcraftsmanship.model.base;


import com.testcraftsmanship.model.authentication.UserType;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
import org.openqa.selenium.firefox.FirefoxProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;

public class DriverFactory {
    private static final Logger LOGGER = LoggerFactory.getLogger(DriverFactory.class);
    private static WebDriver driver;
    private static DriverType driverType = DriverType.FIREFOX_DRIVER;

    enum DriverType {
        FIREFOX_DRIVER
    }
    private DriverFactory(){}

    public static WebDriver getDriver(UserType userType) {
        if (driver == null && driverType.equals(DriverType.FIREFOX_DRIVER)) {
            String geckoDriverPath = DriverFactory.class.getResource("/drivers/geckodriver.exe").getPath();
            System.setProperty("webdriver.gecko.driver", geckoDriverPath);
            driver = new FirefoxDriver(createFirefoxDriverOptions(userType));
        }
        LOGGER.info("Return {}", driverType);
        return driver;
    }

    private static FirefoxOptions createFirefoxDriverOptions(UserType userType) {
        File firefoxProfileFile = new File(DriverFactory.class.getResource(userType.getProfilePath()).getPath());
        FirefoxProfile profile = new FirefoxProfile(firefoxProfileFile);
        profile.setPreference("security.default_personal_cert", "Select Automatically");
        profile.setPreference("intl.accept_languages", "en-gb");
        FirefoxOptions options = new FirefoxOptions();
        options.setProfile(profile);
        return options;
    }
}


The next class we need to update is BaseTest. We need to create here a new method openApplicationAs(UserType userType) which creates new driver with correct profile and navigate to desired web page. When you want to practice on real example I can suggest you to download certificate from here and as a test page use https://client.badssl.com/. The whole class is listed below:

package com.testcraftsmanship.model.base;

import com.testcraftsmanship.model.authentication.UserType;
import com.testcraftsmanship.model.pages.AuthenticationPage;
import org.openqa.selenium.WebDriver;

public abstract class BaseTest implements PageInitializer {
    private WebDriver driver;

    public BaseTest() {
    }

    public WebDriver getDriver() {
        return driver;
    }

    public AuthenticationPage openApplicationAs(UserType userType) {
        driver = DriverFactory.getDriver(userType);
        driver.get("https://client.badssl.com/");
        return newInstance(AuthenticationPage.class);
    }

    protected void closeApplication() {
        driver.quit();
    }
}


That is all in general to set everything up. I am adding a few more classes below to have complete solution with test scenario. I guess that it should be clear so not adding more comments.

package com.testcraftsmanship.model.base;

import org.openqa.selenium.WebDriver;

public abstract class BasePage implements PageInitializer {
    private final WebDriver driver;

    public BasePage(WebDriver driver) {
        this.driver = driver;
    }

    @Override
    public WebDriver getDriver() {
        return driver;
    }
}

package com.testcraftsmanship.model.pages;

import com.testcraftsmanship.model.base.BasePage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;

public class AuthenticationPage extends BasePage {
    @FindBy(id = "footer")
    private WebElement authenticationInfo;

    public AuthenticationPage(WebDriver driver) {
        super(driver);
    }

    public String getAuthenticationInfo() {
        return authenticationInfo.getText();
    }
}

package com.testcraftsmanship;

import com.testcraftsmanship.model.base.BaseTest;
import com.testcraftsmanship.model.pages.AuthenticationPage;
import org.junit.After;
import org.junit.Test;

import static com.testcraftsmanship.model.base.Constant.CUSTOMER_USER;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

public class ClientSideCertificateTest extends BaseTest {

    @Test
    public void itShouldBePossibleToAuthenticateWithClientCert() {
        AuthenticationPage page = openApplicationAs(CUSTOMER_USER.getUserType());
        String authenticationInfo = page.getAuthenticationInfo();
        assertThat(authenticationInfo, equalTo("This site requires a client-authenticated TLS handshake."));
    }

    @After
    public void cleanUp() {
        closeApplication();
    }
}


When you add all those classes then in test you should be able to authenticate with given certificate. Unfortunately this solution works only with Firefox browser. When you want to test client side certificate authentication on other type of browsers then you have to do it on other way e.g. with using proxy, but it is topic for separate article.