Skip to content

Commit

Permalink
2FA changes should invalidate session (#1417)
Browse files Browse the repository at this point in the history
* 2FA changes should invalidate session

* User: invalidate remember me cookie if session buster increases

* Fix 2FA validation on code setup
  • Loading branch information
glaubinix authored Mar 6, 2024
1 parent a39d29c commit c737ff7
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 8 deletions.
1 change: 1 addition & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ security:
always_remember_me: false
secure: '%force_ssl%'
lifetime: 31104000 # 1y
signature_properties: ['password', 'sessionBuster']
logout:
enable_csrf: true
clear_site_data:
Expand Down
9 changes: 6 additions & 3 deletions src/Controller/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace App\Controller;

use App\Attribute\VarName;
use App\Entity\TemporaryTwoFactorUser;
use App\Model\FavoriteManager;
use App\Entity\Package;
use App\Entity\Version;
Expand Down Expand Up @@ -269,13 +270,14 @@ public function enableTwoFactorAuthAction(Request $req, #[VarName('name')] User
// Temporarily store this code on the user, as we'll need it there to generate the
// QR code and to check the confirmation code. We won't actually save this change
// until we've confirmed the code
$user->setTotpSecret($secret);
$temporary2faUser = new TemporaryTwoFactorUser($user, $secret);

$enableRequest = new EnableTwoFactorRequest();
$form = $this->createForm(EnableTwoFactorAuthType::class, $enableRequest, ['user' => $user])
$form = $this->createForm(EnableTwoFactorAuthType::class, $enableRequest, ['user' => $temporary2faUser])
->handleRequest($req);

if ($form->isSubmitted() && $form->isValid()) {
$user->setTotpSecret($secret);
$req->getSession()->remove('2fa_secret');
$authManager->enableTwoFactorAuth($user, $secret);
$backupCode = $authManager->generateAndSaveNewBackupCode($user);
Expand All @@ -287,7 +289,7 @@ public function enableTwoFactorAuthAction(Request $req, #[VarName('name')] User
}

$req->getSession()->set('2fa_secret', $secret);
$qrContent = $authenticator->getQRContent($user);
$qrContent = $authenticator->getQRContent($temporary2faUser);

$qrCode = Builder::create()
->writer(new SvgWriter())
Expand All @@ -305,6 +307,7 @@ public function enableTwoFactorAuthAction(Request $req, #[VarName('name')] User
[
'user' => $user,
'form' => $form,
'tfaConfig' => $temporary2faUser->getTotpAuthenticationConfiguration(),
'qrCode' => $qrCode->getDataUri(),
]
);
Expand Down
31 changes: 31 additions & 0 deletions src/Entity/TemporaryTwoFactorUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace App\Entity;

use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;

class TemporaryTwoFactorUser implements TwoFactorInterface
{
public function __construct(
private readonly User $user,
private readonly string $totpSecret,
) {
}

public function isTotpAuthenticationEnabled(): bool
{
return true;
}

public function getTotpAuthenticationUsername(): string
{
return $this->user->getTotpAuthenticationUsername();
}

public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface
{
return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
}
}
22 changes: 22 additions & 0 deletions src/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ class User implements UserInterface, TwoFactorInterface, BackupCodeInterface, Eq
#[ORM\Column(type: 'string', length: 8, nullable: true)]
private string|null $backupCode = null;

#[ORM\Column(type: 'smallint', options: ['unsigned' => true, 'default' => 0])]
private int $sessionBuster = 0;

public function __construct()
{
$this->packages = new ArrayCollection();
Expand Down Expand Up @@ -242,6 +245,7 @@ public function getGravatarUrl(): string
public function setTotpSecret(string|null $secret): void
{
$this->totpSecret = $secret;
$this->invalidateAllSessions();
}

public function isTotpAuthenticationEnabled(): bool
Expand Down Expand Up @@ -307,6 +311,7 @@ public function __serialize(): array
$this->id,
$this->email,
$this->emailCanonical,
'session_buster' => $this->sessionBuster,
];
}

Expand All @@ -325,6 +330,9 @@ public function __unserialize(array $data): void
$this->email,
$this->emailCanonical
] = $data;

// session_buster might not be available yet in the session
$this->sessionBuster = $data['session_buster'] ?? 0;
}

public function eraseCredentials(): void
Expand Down Expand Up @@ -502,6 +510,10 @@ public function isEqualTo(UserInterface $user): bool
return false;
}

if ($this->getSessionBuster() !== $user->getSessionBuster()) {
return false;
}

return (!$this->getPassword() && !$user->getPassword()) || ($this->getPassword() && $user->getPassword() && hash_equals($this->getPassword(), $user->getPassword()));
}

Expand Down Expand Up @@ -529,4 +541,14 @@ public function isPasswordRequestExpired(int $ttl): bool
{
return !$this->getPasswordRequestedAt() instanceof \DateTime || $this->getPasswordRequestedAt()->getTimestamp() + $ttl <= time();
}

public function getSessionBuster(): int
{
return $this->sessionBuster;
}

public function invalidateAllSessions(): void
{
$this->sessionBuster = ($this->sessionBuster + 1) % 65535;
}
}
4 changes: 2 additions & 2 deletions src/Form/Type/EnableTwoFactorAuthType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@

namespace App\Form\Type;

use App\Entity\User;
use App\Form\Model\EnableTwoFactorRequest;
use App\Validator\TwoFactorCode;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
Expand All @@ -39,7 +39,7 @@ public function configureOptions(OptionsResolver $resolver): void
'data_class' => EnableTwoFactorRequest::class,
])
->define('user')
->allowedTypes(User::class)
->allowedTypes(TwoFactorInterface::class)
->required();
}
}
4 changes: 2 additions & 2 deletions src/Validator/TwoFactorCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

namespace App\Validator;

use App\Entity\User;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
use Symfony\Component\Validator\Constraint;

class TwoFactorCode extends Constraint
{
public function __construct(
public readonly User $user,
public readonly TwoFactorInterface $user,
public readonly string $message = 'Invalid authenticator code',
) {
parent::__construct();
Expand Down
2 changes: 1 addition & 1 deletion templates/user/enable_two_factor_auth.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<p class="pull-right two-factor-key">
<img src="{{ qrCode }}" height="200" />
<br>
<small>TOTP Key: <code style="user-select: all">{{ user.totpAuthenticationConfiguration.secret }}</code></small>
<small>TOTP Key: <code style="user-select: all">{{ tfaConfig.secret }}</code></small>
</p>

<h3>Enabling Two-Factor Authentication</h3>
Expand Down

0 comments on commit c737ff7

Please sign in to comment.