Prechádzať zdrojové kódy

Generate a one-time backup code in case the user can't use their authenticator

Colin O'Dell 5 rokov pred
rodič
commit
fd7d699531

+ 4 - 0
app/config/config.yml

@@ -136,6 +136,10 @@ hwi_oauth:
                 csrf: true
 
 scheb_two_factor:
+    backup_codes:
+        enabled: true
+        manager: Packagist\WebBundle\Security\TwoFactorAuthManager
+
     totp:
         enabled: true
         server_name: '%packagist_host%'

+ 10 - 3
src/Packagist/WebBundle/Controller/UserController.php

@@ -285,7 +285,11 @@ class UserController extends Controller
             throw new AccessDeniedException('You cannot change this user\'s two-factor authentication settings');
         }
 
-        return array('user' => $user);
+        if ($backupCode = $this->get('session')->get('backup_code')) {
+            $this->get('session')->remove('backup_code');
+        }
+
+        return array('user' => $user, 'backup_code' => $backupCode);
     }
 
     /**
@@ -317,9 +321,12 @@ class UserController extends Controller
             }
 
             if ($form->isValid()) {
-                $this->get(TwoFactorAuthManager::class)->enableTwoFactorAuth($user, $enableRequest->getSecret());
+                $authManager = $this->get(TwoFactorAuthManager::class);
+                $authManager->enableTwoFactorAuth($user, $enableRequest->getSecret());
+                $backupCode = $authManager->generateAndSaveNewBackupCode($user);
 
                 $this->addFlash('success', 'Two-factor authentication has been enabled.');
+                $this->get('session')->set('backup_code', $backupCode);
 
                 return $this->redirectToRoute('user_2fa_configure', array('name' => $user->getUsername()));
             }
@@ -341,7 +348,7 @@ class UserController extends Controller
 
         $token = $this->get('security.csrf.token_manager')->getToken('disable_2fa')->getValue();
         if (hash_equals($token, $req->query->get('token', ''))) {
-            $this->get(TwoFactorAuthManager::class)->disableTwoFactorAuth($user);
+            $this->get(TwoFactorAuthManager::class)->disableTwoFactorAuth($user, 'Manually disabled');
 
             $this->addFlash('success', 'Two-factor authentication has been disabled.');
 

+ 28 - 1
src/Packagist/WebBundle/Entity/User.php

@@ -14,6 +14,7 @@ namespace Packagist\WebBundle\Entity;
 
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\ORM\Mapping as ORM;
+use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
 use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface;
 use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
 use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
@@ -56,7 +57,7 @@ use FOS\UserBundle\Model\User as BaseUser;
  *     )
  * })
  */
-class User extends BaseUser implements TwoFactorInterface
+class User extends BaseUser implements TwoFactorInterface, BackupCodeInterface
 {
     /**
      * @ORM\Id
@@ -129,6 +130,12 @@ class User extends BaseUser implements TwoFactorInterface
      */
     private $totpSecret;
 
+    /**
+     * @ORM\Column(type="string", length=8, nullable=true)
+     * @var string|null
+     */
+    private $backupCode;
+
     public function __construct()
     {
         $this->packages = new ArrayCollection();
@@ -347,4 +354,24 @@ class User extends BaseUser implements TwoFactorInterface
     {
         return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6);
     }
+
+    public function isBackupCode(string $code): bool
+    {
+        return $this->backupCode !== null && hash_equals($this->backupCode, $code);
+    }
+
+    public function invalidateBackupCode(string $code): void
+    {
+        $this->backupCode = null;
+    }
+
+    public function invalidateAllBackupCodes(): void
+    {
+        $this->backupCode = null;
+    }
+
+    public function setBackupCode(string $code): void
+    {
+        $this->backupCode = $code;
+    }
 }

+ 1 - 0
src/Packagist/WebBundle/Resources/config/services.yml

@@ -214,6 +214,7 @@ services:
             - '@mailer'
             - '@twig'
             - '@logger'
+            - '@session.flash_bag'
             - { from: '%mailer_from_email%', fromName: '%mailer_from_name%' }
 
     scheduler:

+ 5 - 0
src/Packagist/WebBundle/Resources/views/email/two_factor_disabled.txt.twig

@@ -2,6 +2,11 @@
 
 Two-factor authentication has been disabled on your Packagist account.
 
+-------------------------------
+Time: {{ 'now'|date('c') }}
+Reason: {{ reason }}
+-------------------------------
+
 You can re-enable this at any time from your account page: {{ url('user_2fa_configure', { 'name': username }) }}
 
 {%- endautoescape %}

+ 7 - 0
src/Packagist/WebBundle/Resources/views/user/configure_two_factor_auth.html.twig

@@ -5,6 +5,13 @@
 {% set isActualUser = app.user and app.user.username is same as(user.username) %}
 
 {% block content %}
+    {% if backup_code %}
+        <div class="alert alert-info">
+            <p>If you lose your phone or otherwise can't get codes via your authenticator app, you can this one-time backup code to sign in: <code>{{ backup_code }}</code></p>
+            <p><strong>Take a moment to record this in a safe place. This code will not be shown again!</strong></p>
+        </div>
+    {% endif %}
+
     <h2 class="title">
         {{ user.username }}
         <small>

+ 61 - 3
src/Packagist/WebBundle/Security/TwoFactorAuthManager.php

@@ -14,26 +14,31 @@ namespace Packagist\WebBundle\Security;
 
 use Packagist\WebBundle\Entity\User;
 use Psr\Log\LoggerInterface;
+use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
+use Scheb\TwoFactorBundle\Security\TwoFactor\Backup\BackupCodeManagerInterface;
 use Symfony\Bridge\Doctrine\RegistryInterface;
+use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
 use Twig\Environment;
 
 /**
  * @author Colin O'Dell <colinodell@gmail.com>
  */
-class TwoFactorAuthManager
+class TwoFactorAuthManager implements BackupCodeManagerInterface
 {
     protected $doctrine;
     protected $mailer;
     protected $twig;
     protected $logger;
+    protected $flashBag;
     protected $options;
 
-    public function __construct(RegistryInterface $doctrine, \Swift_Mailer $mailer, Environment $twig, LoggerInterface $logger, array $options)
+    public function __construct(RegistryInterface $doctrine, \Swift_Mailer $mailer, Environment $twig, LoggerInterface $logger, FlashBagInterface $flashBag, array $options)
     {
         $this->doctrine = $doctrine;
         $this->mailer = $mailer;
         $this->twig = $twig;
         $this->logger = $logger;
+        $this->flashBag = $flashBag;
         $this->options = $options;
     }
 
@@ -70,14 +75,17 @@ class TwoFactorAuthManager
      * Disable two-factor auth on the given user account and send confirmation email.
      *
      * @param User   $user
+     * @param string $reason
      */
-    public function disableTwoFactorAuth(User $user)
+    public function disableTwoFactorAuth(User $user, string $reason)
     {
         $user->setTotpSecret(null);
+        $user->invalidateAllBackupCodes();
         $this->doctrine->getManager()->flush();
 
         $body = $this->twig->render('PackagistWebBundle:email:two_factor_disabled.txt.twig', array(
             'username' => $user->getUsername(),
+            'reason' => $reason,
         ));
 
         $message = (new \Swift_Message)
@@ -93,4 +101,54 @@ class TwoFactorAuthManager
             $this->logger->error('['.get_class($e).'] '.$e->getMessage());
         }
     }
+
+    /**
+     * Generate a new backup code and save it on the given user account.
+     *
+     * @param User $user
+     *
+     * @return string
+     */
+    public function generateAndSaveNewBackupCode(User $user): string
+    {
+        $code = bin2hex(random_bytes(4));
+        $user->setBackupCode($code);
+
+        $this->doctrine->getManager()->flush();
+
+        return $code;
+    }
+
+    /**
+     * Check if the code is a valid backup code of the user.
+     *
+     * @param User   $user
+     * @param string $code
+     *
+     * @return bool
+     */
+    public function isBackupCode($user, string $code): bool
+    {
+        if ($user instanceof BackupCodeInterface) {
+            return $user->isBackupCode($code);
+        }
+
+        return false;
+    }
+
+    /**
+     * Invalidate a backup code from a user.
+     *
+     * This should only be called after the backup code has been confirmed and consumed.
+     *
+     * @param User   $user
+     * @param string $code
+     */
+    public function invalidateBackupCode($user, string $code): void
+    {
+        if ($user instanceof BackupCodeInterface) {
+            $this->disableTwoFactorAuth($user, 'Backup code used');
+            $this->flashBag->add('warning', 'Use of your backup code has disabled two-factor authentication for your account. Please consider re-enabling it for your security.');
+        }
+    }
 }