For security reasons two-factor authentication (2FA) is added to a login process, but how can you test this properly when random code is being generated?

Two-factor authentication or 2FA has been a number of years an additional layer of security for web applications. It was already in use for desktop and terminal applications, but it’s slowly getting mainstream for web applications and SaaS solutions.

What is two-factor authentication?

Two-factor authentication is a concept where a person requires a second form of authentication after an initial login with username and password, hence the term two-factor. Especially for web applications, 2FA has become a must in the fight against hacked accounts and breached user credentials on the “Dark Web”.

Two Factor Authentication Process

To provide the second factor, many tools are widely available and each solution has their benefits and of course also some downsides.

How to test two-factor authentication?

There’s great advice on how to test a login screen on the Ministry of Testing Club website so I’m not going into that process here.

Testing 2FA is not a trivial procedure as there are two important elements that are beyond your control:

  1. randomness
  2. external device or system

The random generator for creating a true unique code makes it hard to predict the result, which is a crucial part for testing: predicting the result so you can verify your expectation against a real outcome.

Using an external device (hardware token) or external service (Google 2FA service) also means you’re dealing with something that’s out of your control. Testing this external device or service might not be available in your test environment, or it’s (too) expensive to use in a continuous testing pipeline (SMS or phone calling service).

Functional testing 2FA

Approaching 2FA for functional testing is a straight-forward process for human testers as they can use a the device or service for secondary authentication. But how can you automate this?

For SMS or call services you can make use of an external phone service that will receive the code for you and provides it as value for your 2FA input field. In the example below we make use of Twilio.

Two Factor Authentication Process with Twilio

For scratch cards, hardware tokens or biometric 2FA solutions there’s a physical barrier that will make automated testing very difficult. Unless you have enormous amounts of money to spend on machines that can scratch a scratch card, push a button on a hardware token or to scan body parts of course. In other cases you have to rely on the hardware vendor to have fully tested their products. Ask them if they performed tests and how they tested their product(s).

Unit testing 2FA

When writing unit tests for 2FA is simple as long we concider the 2FA service as an external system which we can stub. Consider the following PHP code for 2FA verification.

/**
 * Validates a provided 2FA code against the 2FA service and throws an
 * Exception when the given code is invalid or will return TRUE on
 * successful verification.
 *
 * @param string $twoFactorCode
 * @return bool
 * @throws \InvalidArgumentException
 */
public function validateTwoFactorCode(string $twoFactorCode): bool
{
    if (!$this->twoFactorService->validateCode($this->accountEntity, $twoFactorCode)) {
        throw new \InvalidArgumentException('Provided 2FA code is invalid');
    }
    return true;
}

We can test this both invalid as valid processing of a provided 2FA code with the following code snipit using PHPUnit where the 2FA service itself doesn’t have to be defined, just following the interface blueprint.

/**
 * Authentication throws exception for invalid two factor code
 *
 * @covers \LoginForm\Auth\Service\AuthenticationService::__construct
 * @covers \LoginForm\Auth\Service\AuthenticationService::validateTwoFactorCode
 * @expectedException \InvalidArgumentException
 */
public function testAuthenticationThrowsExceptionForInvalidTwoFactorCode()
{
    $twoFactorCode = '123456';

    $this->twoFactorServiceMock->expects($this->once())
        ->method('validateCode')
        ->willReturn(false);

    $authService = new AuthenticationService(
        $this->validator,
        $this->accountModel,
        $this->accountEntity,
        $this->twoFactorServiceMock
    );
    $authService->validateTwoFactorCode($twoFactorCode);
    $this->fail('Expected exception was not triggered for invalid 2FA code');
}

/**
 * Authentication accepts valid two factor code
 *
 * @covers \LoginForm\Auth\Service\AuthenticationService::__construct
 * @covers \LoginForm\Auth\Service\AuthenticationService::validateTwoFactorCode
 */
public function testAuthenticationAcceptsValidTwoFactorCode()
{
    $twoFactorCode = '123456';

    $this->twoFactorServiceMock->expects($this->once())
        ->method('validateCode')
        ->willReturn(true);

    $authService = new AuthenticationService(
        $this->validator,
        $this->accountModel,
        $this->accountEntity,
        $this->twoFactorServiceMock
    );
    $validTwoFactorCode = $authService->validateTwoFactorCode($twoFactorCode);
    $this->assertTrue($validTwoFactorCode,
        sprintf('Expected 2FA code "%s" to be valid', $twoFactorCode)
    );
}

This will result in successful processing of 2FA authentication without actually implementing the service.

Two Factor Authentication (2FA) Testing with PHPUnit

Resilience Testing 2FA

What if the 2FA system fails in one way or another? Are there backup alternatives to get access to an account?

In a best scenario you offer 2FA with backup codes and you provide an option to enter backup codes. When a backup code is used, it should be removed immediately from the backend so it cannot be used again.

Other services offer multiple forms of 2FA:

  • Use a U2F FIDO key as primary form of authentication
  • Use a mobile app as secondary form of authentication
  • Use a mobile number to send SMS or voice call with authentication code

When using a third-party service to generate, send or validate a 2FA code, you must keep in mind that these services might not be available. How are you handling this scenario?

Offering multiple ways for authentication is crucial to overcome one or more services to fail. You offer two-factor authentication as a better form of security for your users and you should not compromise on that elevated security. So never disable 2FA when your dependend services are down, instead offer a clear message stating that the service is out and to protect your user’s security you don’t allow non-validated access.

Other testing

No argument should stop you from testing 2FA, especially because you offer it to increase security for your users. So if you have time and budget add other forms of testing in your validation of the service. Your users’s safety depends on it.

A word of caution: when using automated acceptance testing, don’t create some form of security bypass to make the test pass! Testing the functionality is important, but not at the expense of security. If there’s a security bypass, it will be found.

Closing remarks

First of all a big appreciation and gratitude for offering two-factor authentication as an additional security layer and your willingness to test these services to ensure you offer a safe and secure form of protection.

If you want to get started with two-factor authentication, you might want to read the documentation Google offers for 2FA. And if you’re interested in setting up U2F FIDO key authentication, I can highly advise you to read the documentation Yubico provides.

php[architect] Magazine August 2017

Source: php[architect]

This month’s edition of php[architect] magazine is covering how to set up Single Sign On (SSO) with two-factor authentication. So if you’re looking to implement such a system for your users, I can highly recommend reading about it.

Why not subscribe to their magazine and get each month awesome articles and great code samples?

You can get a monthly subscription for as low as $ 4.99, but they offer also great money savers for yearly subcriptions and businesses.

See php[architect] magazine subcription page for more details.

SaveSave

SaveSave


Michelangelo van Dam

Michelangelo van Dam is a senior PHP architect, PHP community leader and international conference speaker with many contributions to PHP projects and community events.

Related Posts

Knowledge

Mocking final classes in unit tests

A quick solution to remove dependency on a final class in your unit test using reflection.

Knowledge

PHP7 workshop at DrukwerkDeal

On February 12, 2016 our very own Michelangelo van Dam will be giving a PHP7 workshop at Drukwerkdeal in Deventer. Related

Knowledge

Automated database migrations with DBDeploy

Web application development has become a really professional industry over the past decades, where professional teams are ensuring web applications get updated in an automated, tested and secured fashion. Concepts like “Continuous Integration”, “Continuous Deployment” Read more…