Dealing with 2FA

 



Recently I had to enable 2FA for Github since I have projects on Github or I would not be able to publish code changes. It reminds me that my library Apie has no 2FA value objects yet.

The intention of 2FA

Normally you login with a username and a password, and you can login on any computer on the internet in the entire world. This could be a bad thing if your password is leaked on the internet because everybody can pretend to be you. For this reason many websites want a second check that proves that you own a specific object or device by entering a 2nd code. This code has to change every time and it needs to be synced between this app/device and the server. Basically this is what 2FA means.
In practice however it often means you get a code with SMS or you have an app like Authy or Google Authenticator installed on your mobile phone.
SMS with 2FA is considered unsafe. If you login on your mobile it is technically not even 2FA since there is no second device.

How Google Authenticator and Authy work

Authy or Google Authenticator use a technique called HOTP(HMAC-based One Time Password) or TOTP(Time-based One Time Password) for generating codes.
With HOTP you use a secret code only known by google authenticator or authy and a counter how many times you generated a password. If you use one password when logging in, the counter is increased at the server and your Authenticator app requires to increase the counter as well after being used.
With TOTP you use a secret code only known by google authenticator or authy and use the current time for a new code.
In general the secret code should only be known by the server and by the Google Authenticator/Authy application. Also if you login on the device that has the secret code, then technically it is not 2FA.
Also the way how the secret code is sent to Google Authenticator/Authy is the only thing communicated that should not leak to make it really 2FA. This is often communicated with a QR code. If I would make a screenshot of the QR code I could technically link it to multiple devices since they all know the secret code.

How Apie deals with 2FA

Apie provides the package apie/otp-value-objects to offer a few value objects for 2FA. You can use these in your User object or any other object. I can not generalize this as it depends on the application how 2FA should work. It could be possible to also use an external authentication service like keycloak and not write any code in your application. But if this is undesirable we can make it work in Apie very easily. It depends on the project if:
  • Is it required for all new users?
  • Are there existing users before 2FA was added and do they need to activate 2FA?
  • When do they configure/reset their 2FA?
  • How can I reset a user 2FA?
  • Can an inactive or blocked user reset/enable 2FA?
First call composer require apie/otp-value-objects:1.0.0.x-dev in a terminal to install the otp value objects in your application.

In this example I expect 2FA to be optional per user and we use TOTP:

use Apie\OtpValueObjects\OTP;
use Apie\OtpValueObjects\TotpSecret;
class User implements EntityInterface
{
    private TotpSecret $totp = TotpSecret::createRandom();
    
    private bool $totpActivated = false;
    // some code
    
    public function reset2FA(): void
    {
        $this->totp = TotpSecret::createRandom();
        $this->totpActivated = false;
    }
    
    public function enable2FA(OTP $otp): void
    {
        if ($this->totp->verify($otp)) {
            $this->totpActivated = true;
        }
    }
    
    public function verify(string $password, ?OTP $otp = null): bool
    {
        if (!$this->encryptedPassword->verify($password)) {
            return false;
        }
        if ($this->totpActivated && $totp) {
            return $this->totp->verify($otp);
        }
        
        return !$this->totpActivated;
    }
}
The above example will add the following endpoints to your Apie API automatically:
  • GET or POST /user/{id}/reset2FA
  • POST /user/{id}/enable2FA
  • POST /user/{id}/verify
If you started with the apie project starter, User::verify() is used for the login function in apie/cms and adding the argument will enable 2FA right away. if 2FA is required all you need to do is change the second argument $otp by removing the default value null and removing the question mark on the typehint. It makes property $totpActivated also a bit redundant.

HOTP example

If you want to create a HOTP you need an additional step in the verify method as you need to increase the counter every time you verify it. Since value objects are immutable, the counter is not increased internally in the HotpSecret value object and an update requires setting property $hotp again.

if ($this->hotpActivated && $otp) {
    $verified = $this->hotp->verify($otp);
    $this->hotp = $this->hotp->nextPassword();
    return $verified;
}

apie/cms support

With some tinkering I managed to make it work in apie/cms for activating 2FA on users with a working QR code. The QR code is created with chillerlan/php-qrcode. Because you only get the activate token in a enable2FA action, I had to make a new value object construction to link the form to the actual secret. 

To add a verify token action you need to extend a specific class and provide 2 static method implementations: one for the label in google authenticator and one for linking it to a property field with the secret in the User object. See example of the one I'm going to add in the project starter and the typehint you should use in your User::enable2FA action:

namespace App\Apie\Example\ValueObjects;

use Apie\Core\Entities\EntityInterface;
use Apie\OtpValueObjects\VerifyOTP;
use App\Apie\Example\Resources\User;
use ReflectionProperty;

final class VerifyUserOTP extends VerifyOTP
{
    public static function getOtpReference(): ReflectionProperty
    {
        return new ReflectionProperty(User::class, 'totp');
    }

    public static function getOtpLabel(EntityInterface $entity): string
    {
        assert($entity instanceof User);
        return 'Apie test project (' . $entity->getId() . ')';
    }
}

With this example scanning the QR code in apie/cms with Authy or Google Authenticator will be displayed as "Apie test project (<e-mail address user>)" and the secret comes from the property $totp inside the user class.








Comments