-
Notifications
You must be signed in to change notification settings - Fork 75
Description
Context: I am currently migrating a project to the experimental AuthenticationManager as well as using this bundle instead of a homegrown 2fa solution.
We do have an API with an existing endpoint for logging in users which returns a special code to the client if 2fa is required, and then expects the login API call again with username, password, AND the 2fa code all present in one to do authentication in a single request.
This does not really play well with your API guidelines which require a two-request flow, which IMO is more complex as the client needs to keep even more state.
Anyway I ended up implementing this myself as it isn't that hard, but I also got pretty familiar with the new auth code as I had to do various authenticator implementations. So I am mostly posting this here for others who may need the help to figure this out.. and I am thinking if this bundle offered better support for it in the future it would maybe be good (could be a v6 thing for sure).
I now create this badge in my API authenticator, which gets added on the passport:
new TwoFactorAuthBadge($credentials['2fa_code'] ?? null)
The badge class is a very simple value object:
<?php
class TwoFactorAuthBadge implements BadgeInterface
{
private $resolved = false;
private ?string $twoFaCode;
public function __construct(?string $twoFaCode)
{
$this->twoFaCode = $twoFaCode;
}
public function getTwoFaCode(): ?string
{
return $this->twoFaCode;
}
/**
* @internal
*/
public function markResolved(): void
{
$this->resolved = true;
}
public function isResolved(): bool
{
return $this->resolved;
}
}And the auth listener which checks the badge is also not that complex, which is why I think it might be worth including here:
<?php
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
class TwoFactorAuthListener implements EventSubscriberInterface
{
private TotpAuthenticatorInterface $totpAuthenticator;
public function __construct(TotpAuthenticatorInterface $totpAuthenticator)
{
$this->totpAuthenticator = $totpAuthenticator;
}
public function checkPassport(CheckPassportEvent $event): void
{
/** @var Passport $passport */
$passport = $event->getPassport();
if (!$passport->hasBadge(TwoFactorAuthBadge::class)) {
return;
}
/** @var TwoFactorAuthBadge $badge */
$badge = $passport->getBadge(TwoFactorAuthBadge::class);
if ($badge->isResolved()) {
return;
}
/** @var User $user */
$user = $passport->getUser();
if (!$user->getTwoFaActive()) {
$badge->markResolved();
return;
}
if (null === $badge->getTwoFaCode()) {
throw new MissingTwoFaCodeException();
}
if (false === $this->totpAuthenticator->checkCode($user, $badge->getTwoFaCode())) {
throw new InvalidTwoFaCodeException();
}
$badge->markResolved();
}
public static function getSubscribedEvents(): array
{
return [CheckPassportEvent::class => ['checkPassport', 512]];
}
}There are also two special exceptions MissingTwoFaCodeException and InvalidTwoFaCodeException both extending AuthenticationException that I added to be able to render these correctly in onAuthenticationFailure.
Note: Doing this way, I did not configure 2fa on the API firewall at all, as it's handled purely within the Authenticator via the badge.