Merge branch '2fa' into master

This commit is contained in:
Jan Böhmer 2020-01-01 15:49:42 +01:00
commit 1016f0d4ee
65 changed files with 4769 additions and 320 deletions

View file

@ -21,7 +21,11 @@
namespace App\Controller;
use App\Entity\Parts\Part;
use App\Entity\UserSystem\U2FKey;
use App\Entity\UserSystem\User;
use App\Services\PasswordResetManager;
use App\Services\TFA\BackupCodeManager;
use Doctrine\ORM\EntityManagerInterface;
use Gregwar\CaptchaBundle\Type\CaptchaType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -29,6 +33,7 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Mailer\MailerInterface;

View file

@ -25,16 +25,22 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\UserAttachment;
use App\Entity\UserSystem\User;
use App\Form\Permissions\PermissionsType;
use App\Form\TFAGoogleSettingsType;
use App\Form\UserAdminForm;
use App\Form\UserSettingsType;
use App\Services\EntityExporter;
use App\Services\EntityImporter;
use App\Services\StructuralElementRecursionHelper;
use App\Services\TFA\BackupCodeManager;
use Doctrine\ORM\EntityManagerInterface;
use \Exception;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
use Symfony\Component\Asset\Packages;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@ -61,6 +67,29 @@ class UserController extends AdminPages\BaseAdminController
*/
public function edit(User $entity, Request $request, EntityManagerInterface $em)
{
//Handle 2FA disabling
if($request->request->has('reset_2fa')) {
//Check if the admin has the needed permissions
$this->denyAccessUnlessGranted('set_password', $entity);
if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) {
//Disable Google authenticator
$entity->setGoogleAuthenticatorSecret(null);
$entity->setBackupCodes([]);
//Remove all U2F keys
foreach($entity->getU2FKeys() as $key) {
$em->remove($key);
}
//Invalidate trusted devices
$entity->invalidateTrustedDeviceTokens();
$em->flush();
$this->addFlash('success', 'user.edit.reset_success');
} else {
$this->addFlash('danger', 'csfr_invalid');
}
}
return $this->_edit($entity, $request, $em);
}
@ -76,7 +105,7 @@ class UserController extends AdminPages\BaseAdminController
}
/**
* @Route("/{id}", name="user_delete", methods={"DELETE"})
* @Route("/{id}", name="user_delete", methods={"DELETE"}, requirements={"id"="\d+"})
*/
public function delete(Request $request, User $entity, StructuralElementRecursionHelper $recursionHelper)
{
@ -147,94 +176,6 @@ class UserController extends AdminPages\BaseAdminController
]);
}
/**
* @Route("/settings", name="user_settings")
*/
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder)
{
/**
* @var User
*/
$user = $this->getUser();
$page_need_reload = false;
if (!$user instanceof User) {
return new \RuntimeException('This controller only works only for Part-DB User objects!');
}
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
/***************************
* User settings form
***************************/
$form = $this->createForm(UserSettingsType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Check if user theme setting has changed
if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) {
$page_need_reload = true;
}
$em->flush();
$this->addFlash('success', 'user.settings.saved_flash');
}
/*****************************
* Password change form
****************************/
$demo_mode = $this->getParameter('demo_mode');
$pw_form = $this->createFormBuilder()
->add('old_password', PasswordType::class, [
'label' => 'user.settings.pw_old.label',
'disabled' => $demo_mode,
'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted.
->add('new_password', RepeatedType::class, [
'disabled' => $demo_mode,
'type' => PasswordType::class,
'first_options' => ['label' => 'user.settings.pw_new.label'],
'second_options' => ['label' => 'user.settings.pw_confirm.label'],
'invalid_message' => 'password_must_match',
'constraints' => [new Length([
'min' => 6,
'max' => 128,
])],
])
->add('submit', SubmitType::class, ['label' => 'save'])
->getForm();
$pw_form->handleRequest($request);
//Check if password if everything was correct, then save it to User and DB
if ($pw_form->isSubmitted() && $pw_form->isValid()) {
$password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData());
$user->setPassword($password);
//After the change reset the password change needed setting
$user->setNeedPwChange(false);
$em->persist($user);
$em->flush();
$this->addFlash('success', 'user.settings.pw_changed_flash');
}
/******************************
* Output both forms
*****************************/
return $this->render('Users/user_settings.html.twig', [
'settings_form' => $form->createView(),
'pw_form' => $pw_form->createView(),
'page_need_reload' => $page_need_reload,
]);
}
/**
* Get either a Gravatar URL or complete image tag for a specified email address.
*

View file

@ -0,0 +1,308 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Controller;
use App\Entity\UserSystem\U2FKey;
use App\Entity\UserSystem\User;
use App\Form\TFAGoogleSettingsType;
use App\Form\UserSettingsType;
use App\Services\TFA\BackupCodeManager;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
use Symfony\Component\Validator\Constraints\Length;
/**
* @Route("/user")
* @package App\Controller
*/
class UserSettingsController extends AbstractController
{
protected $demo_mode;
public function __construct(bool $demo_mode)
{
$this->demo_mode = $demo_mode;
}
/**
* @Route("/2fa_backup_codes", name="show_backup_codes")
*/
public function showBackupCodes()
{
$user = $this->getUser();
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if (!$user instanceof User) {
return new \RuntimeException('This controller only works only for Part-DB User objects!');
}
if (empty($user->getBackupCodes())) {
$this->addFlash('error', 'tfa_backup.no_codes_enabled');
throw new Exception('You do not have any backup codes enabled, therefore you can not view them!');
}
return $this->render('Users/backup_codes.html.twig', [
'user' => $user
]);
}
/**
* @Route("/u2f_delete", name="u2f_delete", methods={"DELETE"})
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager)
{
if($this->demo_mode) {
throw new \RuntimeException('You can not do 2FA things in demo mode');
}
$user = $this->getUser();
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if (!$user instanceof User) {
throw new \RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
if($request->request->has('key_id')) {
$key_id = $request->request->get('key_id');
$key_repo = $entityManager->getRepository(U2FKey::class);
/** @var U2FKey|null $u2f */
$u2f = $key_repo->find($key_id);
if($u2f === null) {
$this->addFlash('danger','tfa_u2f.u2f_delete.not_existing');
throw new \RuntimeException('Key not existing!');
}
//User can only delete its own U2F keys
if ($u2f->getUser() !== $user) {
$this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
throw new \RuntimeException('You can only delete your own U2F keys!');
}
$backupCodeManager->disableBackupCodesIfUnused($user);
$entityManager->remove($u2f);
$entityManager->flush();
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
}
} else {
$this->addFlash('error','csfr_invalid');
}
return $this->redirectToRoute('user_settings');
}
/**
* @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"})
*/
public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager)
{
if($this->demo_mode) {
throw new \RuntimeException('You can not do 2FA things in demo mode');
}
$user = $this->getUser();
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if (!$user instanceof User) {
return new \RuntimeException('This controller only works only for Part-DB User objects!');
}
if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
$user->invalidateTrustedDeviceTokens();
$entityManager->flush();
$this->addFlash('success', 'tfa_trustedDevice.invalidate.success');
} else {
$this->addFlash('error','csfr_invalid');
}
return $this->redirectToRoute('user_settings');
}
/**
* @Route("/settings", name="user_settings")
*/
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager)
{
/**
* @var User
*/
$user = $this->getUser();
$page_need_reload = false;
//When user change its settings, he should be logged in fully.
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
if (!$user instanceof User) {
throw new \RuntimeException('This controller only works only for Part-DB User objects!');
}
/***************************
* User settings form
***************************/
$form = $this->createForm(UserSettingsType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid() && !$this->demo_mode) {
//Check if user theme setting has changed
if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) {
$page_need_reload = true;
}
$em->flush();
$this->addFlash('success', 'user.settings.saved_flash');
}
/*****************************
* Password change form
****************************/
$pw_form = $this->createFormBuilder()
//Username field for autocomplete
->add('username', TextType::class, [
'data' => $user->getName(),
'attr' => ['autocomplete' => 'username'],
'disabled' => true,
'row_attr' => ['class' => 'd-none']
])
->add('old_password', PasswordType::class, [
'label' => 'user.settings.pw_old.label',
'disabled' => $this->demo_mode,
'attr' => ['autocomplete' => 'current-password'],
'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted.
->add('new_password', RepeatedType::class, [
'disabled' => $this->demo_mode,
'type' => PasswordType::class,
'first_options' => ['label' => 'user.settings.pw_new.label'],
'second_options' => ['label' => 'user.settings.pw_confirm.label'],
'invalid_message' => 'password_must_match',
'options' => [
'attr' => ['autocomplete' => 'new-password']
],
'constraints' => [new Length([
'min' => 6,
'max' => 128,
])],
])
->add('submit', SubmitType::class, ['label' => 'save'])
->getForm();
$pw_form->handleRequest($request);
//Check if password if everything was correct, then save it to User and DB
if ($pw_form->isSubmitted() && $pw_form->isValid() && !$this->demo_mode) {
$password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData());
$user->setPassword($password);
//After the change reset the password change needed setting
$user->setNeedPwChange(false);
$em->persist($user);
$em->flush();
$this->addFlash('success', 'user.settings.pw_changed_flash');
}
//Handle 2FA things
$google_form = $this->createForm(TFAGoogleSettingsType::class, $user);
$google_enabled = $user->isGoogleAuthenticatorEnabled();
if (!$form->isSubmitted() && !$google_enabled) {
$user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
$google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret());
}
$google_form->handleRequest($request);
if($google_form->isSubmitted() && $google_form->isValid() && !$this->demo_mode) {
if (!$google_enabled) {
//Save 2FA settings (save secrets)
$user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
$backupCodeManager->enableBackupCodes($user);
$em->flush();
$this->addFlash('success', 'user.settings.2fa.google.activated');
return $this->redirectToRoute('user_settings');
} elseif ($google_enabled) {
//Remove secret to disable google authenticator
$user->setGoogleAuthenticatorSecret(null);
$backupCodeManager->disableBackupCodesIfUnused($user);
$em->flush();
$this->addFlash('success', 'user.settings.2fa.google.disabled');
return $this->redirectToRoute('user_settings');
}
}
$backup_form = $this->get('form.factory')->createNamedBuilder('backup_codes')->add('reset_codes', SubmitType::class,[
'label' => 'tfa_backup.regenerate_codes',
'attr' => ['class' => 'btn-danger'],
'disabled' => empty($user->getBackupCodes())
])->getForm();
$backup_form->handleRequest($request);
if ($backup_form->isSubmitted() && $backup_form->isValid() && !$this->demo_mode) {
$backupCodeManager->regenerateBackupCodes($user);
$em->flush();
$this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated');
}
/******************************
* Output both forms
*****************************/
return $this->render('Users/user_settings.html.twig', [
'user' => $user,
'settings_form' => $form->createView(),
'pw_form' => $pw_form->createView(),
'page_need_reload' => $page_need_reload,
'google_form' => $google_form->createView(),
'backup_form' => $backup_form->createView(),
'tfa_google' => [
'enabled' => $google_enabled,
'qrContent' => $googleAuthenticator->getQRContent($user),
'secret' => $user->getGoogleAuthenticatorSecret(),
'username' => $user->getGoogleAuthenticatorUsername()
]
]);
}
}

View file

@ -53,9 +53,9 @@ trait MasterAttachmentTrait
* Sets the new master picture for this part.
*
* @param Attachment|null $new_master_attachment
* @return Part
* @return $this
*/
public function setMasterPictureAttachment(?Attachment $new_master_attachment): self
public function setMasterPictureAttachment(?Attachment $new_master_attachment)
{
$this->master_picture_attachment = $new_master_attachment;

View file

@ -107,11 +107,11 @@ class Storelocation extends PartsContainingDBElement
protected $storage_type;
/**
* @ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY")
* @ORM\JoinTable(name="part_lots",
* joinColumns={@ORM\JoinColumn(name="id_store_location", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="id_part", referencedColumnName="id")}
* )
* //@ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY")
* //@ORM\JoinTable(name="part_lots",
* // joinColumns={@ORM\JoinColumn(name="id_store_location", referencedColumnName="id")},
* // inverseJoinColumns={@ORM\JoinColumn(name="id_part", referencedColumnName="id")}
* //)
*/
protected $parts;

View file

@ -106,11 +106,11 @@ class Supplier extends Company
protected $shipping_costs;
/**
* @ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY")
* @ORM\JoinTable(name="orderdetails",
* joinColumns={@ORM\JoinColumn(name="id_supplier", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="part_id", referencedColumnName="id")}
* )
* //@ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY")
* //@ORM\JoinTable(name="orderdetails",
* // joinColumns={@ORM\JoinColumn(name="id_supplier", referencedColumnName="id")},
* // inverseJoinColumns={@ORM\JoinColumn(name="part_id", referencedColumnName="id")}
* //)
*/
protected $parts;

View file

@ -64,12 +64,40 @@ class Group extends StructuralDBElement implements HasPermissionsInterface
*/
protected $permissions;
/**
* @var bool If true all users associated with this group must have enabled some kind of 2 factor authentication
* @ORM\Column(type="boolean", name="enforce_2fa")
*/
protected $enforce2FA = false;
public function __construct()
{
parent::__construct();
$this->permissions = new PermissionsEmbed();
}
/**
* Check if the users of this group are enforced to have two factor authentification (2FA) enabled.
* @return bool
*/
public function isEnforce2FA(): bool
{
return $this->enforce2FA;
}
/**
* Sets if the user of this group are enforced to have two factor authentification enabled.
* @param bool $enforce2FA True, if the users of this group are enforced to have 2FA enabled.
* @return $this
*/
public function setEnforce2FA(bool $enforce2FA): Group
{
$this->enforce2FA = $enforce2FA;
return $this;
}
/**
* Returns the ID as an string, defined by the element class.
* This should have a form like P000014, for a part with ID 14.

View file

@ -0,0 +1,183 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Entity\UserSystem;
use App\Entity\Base\TimestampTrait;
use Doctrine\ORM\Mapping as ORM;
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface;
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
use u2flib_server\Registration;
/**
* @ORM\Entity
* @ORM\Table(name="u2f_keys",
* uniqueConstraints={@ORM\UniqueConstraint(name="user_unique",columns={"user_id",
* "key_handle"})})
* @ORM\HasLifecycleCallbacks()
*/
class U2FKey implements TwoFactorKeyInterface
{
use TimestampTrait;
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string")
* @var string
**/
public $keyHandle;
/**
* @ORM\Column(type="string")
* @var string
**/
public $publicKey;
/**
* @ORM\Column(type="text")
* @var string
**/
public $certificate;
/**
* @ORM\Column(type="string")
* @var int
**/
public $counter;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="u2fKeys")
* @var User
**/
protected $user;
/**
* @ORM\Column(type="string")
* @var string
**/
protected $name;
public function fromRegistrationData(Registration $data): void
{
$this->keyHandle = $data->keyHandle;
$this->publicKey = $data->publicKey;
$this->certificate = $data->certificate;
$this->counter = $data->counter;
}
/** @inheritDoc */
public function getKeyHandle()
{
return $this->keyHandle;
}
/** @inheritDoc */
public function setKeyHandle($keyHandle)
{
$this->keyHandle = $keyHandle;
}
/** @inheritDoc */
public function getPublicKey()
{
return $this->publicKey;
}
/** @inheritDoc */
public function setPublicKey($publicKey)
{
$this->publicKey = $publicKey;
}
/** @inheritDoc */
public function getCertificate()
{
return $this->certificate;
}
/** @inheritDoc */
public function setCertificate($certificate)
{
$this->certificate = $certificate;
}
/** @inheritDoc */
public function getCounter()
{
return $this->counter;
}
/** @inheritDoc */
public function setCounter($counter)
{
$this->counter = $counter;
}
/** @inheritDoc */
public function getName()
{
return $this->name;
}
/** @inheritDoc */
public function setName($name)
{
$this->name = $name;
}
/**
* Gets the user, this U2F key belongs to.
* @return User
*/
public function getUser() : User
{
return $this->user;
}
/**
* The primary key ID of this key
* @return int
*/
public function getID() : int
{
return $this->id;
}
/**
* Sets the user this U2F key belongs to.
* @param TwoFactorInterface $new_user
* @return $this
*/
public function setUser(TwoFactorInterface $new_user) : self
{
$this->user = $new_user;
return $this;
}
}

View file

@ -53,28 +53,38 @@ namespace App\Entity\UserSystem;
use App\Entity\Attachments\AttachmentContainingDBElement;
use App\Entity\Attachments\UserAttachment;
use App\Entity\Base\MasterAttachmentTrait;
use App\Entity\Base\NamedDBElement;
use App\Entity\PriceInformations\Currency;
use App\Security\Interfaces\HasPermissionsInterface;
use App\Validator\Constraints\Selectable;
use App\Validator\Constraints\ValidPermission;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
use Scheb\TwoFactorBundle\Model\PreferredProviderInterface;
use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface as U2FTwoFactorInterface;
/**
* This entity represents a user, which can log in and have permissions.
* Also this entity is able to save some informations about the user, like the names, email-address and other info.
* Also this entity is able to save some informations about the user, like the names, email-address and other info.
*
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Table("`users`")
* @UniqueEntity("name", message="validator.user.username_already_used")
*/
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface,
TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, U2FTwoFactorInterface, PreferredProviderInterface
{
use MasterAttachmentTrait;
/** The User id of the anonymous user */
public const ID_ANONYMOUS = 1;
@ -172,6 +182,33 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
*/
protected $group;
/**
* @var string|null The secret used for google authenticator
* @ORM\Column(name="google_authenticator_secret", type="string", nullable=true)
*/
protected $googleAuthenticatorSecret;
/**
* @var string[]|null A list of backup codes that can be used, if the user has no access to its Google Authenticator device
* @ORM\Column(type="json")
*/
protected $backupCodes = [];
/** @var \DateTime The time when the backup codes were generated
* @ORM\Column(type="datetime", nullable=true)
*/
protected $backupCodesGenerationDate;
/** @var int The version of the trusted device cookie. Used to invalidate all trusted device cookies at once.
* @ORM\Column(type="integer")
*/
protected $trustedDeviceCookieVersion = 0;
/** @var Collection<TwoFactorKeyInterface>
* @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true)
*/
protected $u2fKeys;
/**
* @var array
* @ORM\Column(type="json")
@ -227,6 +264,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
{
parent::__construct();
$this->permissions = new PermissionsEmbed();
$this->u2fKeys = new ArrayCollection();
}
/**
@ -457,6 +495,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
return sprintf('%s %s', $this->getFirstName(), $this->getLastName());
}
/**
* Change the username of this user
* @param string $new_name The new username.
* @return $this
*/
public function setName(string $new_name): NamedDBElement
{
// Anonymous user is not allowed to change its username
@ -468,7 +511,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @return string
* Get the first name of the user.
* @return string|null
*/
public function getFirstName(): ?string
{
@ -476,9 +520,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $first_name
* Change the first name of the user
* @param string $first_name The new first name
*
* @return User
* @return $this
*/
public function setFirstName(?string $first_name): self
{
@ -488,7 +533,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @return string
* Get the last name of the user
* @return string|null
*/
public function getLastName(): ?string
{
@ -496,9 +542,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $last_name
* Change the last name of the user
* @param string $last_name The new last name
*
* @return User
* @return $this
*/
public function setLastName(?string $last_name): self
{
@ -508,6 +555,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* Gets the department of this user
* @return string
*/
public function getDepartment(): ?string
@ -516,8 +564,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $department
*
* Change the department of the user
* @param string $department The new department
* @return User
*/
public function setDepartment(?string $department): self
@ -528,6 +576,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* Get the email of the user.
* @return string
*/
public function getEmail(): ?string
@ -536,9 +585,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $email
*
* @return User
* Change the email of the user
* @param string $email The new email adress
* @return $this
*/
public function setEmail(?string $email): self
{
@ -548,7 +597,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @return string
* Gets the language the user prefers (as 2 letter ISO code).
* @return string|null The 2 letter ISO code of the preferred language (e.g. 'en' or 'de').
* If null is returned, the user has not specified a language and the server wide language should be used.
*/
public function getLanguage(): ?string
{
@ -556,19 +607,21 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $language
*
* Change the language the user prefers.
* @param string|null $language The new language as 2 letter ISO code (e.g. 'en' or 'de').
* Set to null, to use the system wide language.
* @return User
*/
public function setLanguage(?string $language): self
{
$this->language = $language;
return $this;
}
/**
* @return string
* Gets the timezone of the user
* @return string|null The timezone of the user (e.g. 'Europe/Berlin') or null if the user has not specified
* a timezone (then the global one should be used)
*/
public function getTimezone(): ?string
{
@ -576,9 +629,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $timezone
*
* @return User
* Change the timezone of this user.
* @param string $timezone|null The new timezone (e.g. 'Europe/Berlin') or null to use the system wide one.
* @return $this
*/
public function setTimezone(?string $timezone): self
{
@ -588,7 +641,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @return string
* Gets the theme the users wants to see. See self::AVAILABLE_THEMES for valid values.
* @return string|null The name of the theme the user wants to see, or null if the system wide should be used.
*/
public function getTheme(): ?string
{
@ -596,9 +650,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
}
/**
* @param string $theme
*
* @return User
* Change the theme the user wants to see.
* @param string|null $theme The name of the theme (See See self::AVAILABLE_THEMES for valid values). Set to null
* if the system wide theme should be used.
* @return $this
*/
public function setTheme(?string $theme): self
{
@ -607,11 +662,20 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
return $this;
}
/**
* Gets the group to which this user belongs to.
* @return Group|null The group of this user. Null if this user does not have a group.
*/
public function getGroup(): ?Group
{
return $this->group;
}
/**
* Sets the group of this user.
* @param Group|null $group The new group of this user. Set to null if this user should not have a group.
* @return $this
*/
public function setGroup(?Group $group): self
{
$this->group = $group;
@ -619,10 +683,181 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
return $this;
}
/**
* Returns a string representation of this user (the full name).
* E.g. 'Jane Doe (j.doe) [DISABLED]
* @return string
*/
public function __toString()
{
$tmp = $this->isDisabled() ? ' [DISABLED]' : '';
return $this->getFullName(true).$tmp;
}
/**
* Return true if the user should do two-factor authentication.
*
* @return bool
*/
public function isGoogleAuthenticatorEnabled(): bool
{
return $this->googleAuthenticatorSecret ? true : false;
}
/**
* Return the user name that should be shown in Google Authenticator.
* @return string
*/
public function getGoogleAuthenticatorUsername(): string
{
return $this->getUsername();
}
/**
* Return the Google Authenticator secret
* When an empty string is returned, the Google authentication is disabled.
*
* @return string|null
*/
public function getGoogleAuthenticatorSecret(): ?string
{
return $this->googleAuthenticatorSecret;
}
/**
* Sets the secret used for Google Authenticator. Set to null to disable Google Authenticator.
* @param string|null $googleAuthenticatorSecret
* @return $this
*/
public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): self
{
$this->googleAuthenticatorSecret = $googleAuthenticatorSecret;
return $this;
}
/**
* Check if the given code is a valid backup code.
*
* @param string $code The code that should be checked.
* @return bool True if the backup code is valid.
*/
public function isBackupCode(string $code): bool
{
return in_array($code, $this->backupCodes);
}
/**
* Invalidate a backup code.
*
* @param string $code The code that should be invalidated
*/
public function invalidateBackupCode(string $code): void
{
$key = array_search($code, $this->backupCodes);
if ($key !== false){
unset($this->backupCodes[$key]);
}
}
/**
* Returns the list of all valid backup codes
* @return string[] An array with all backup codes
*/
public function getBackupCodes() : array
{
return $this->backupCodes ?? [];
}
/**
* Set the backup codes for this user. Existing backup codes are overridden.
* @param string[] $codes A
* @return $this
*/
public function setBackupCodes(array $codes) : self
{
$this->backupCodes = $codes;
if(empty($codes)) {
$this->backupCodesGenerationDate = null;
} else {
$this->backupCodesGenerationDate = new \DateTime();
}
return $this;
}
/**
* Return the date when the backup codes were generated.
* @return \DateTime|null
*/
public function getBackupCodesGenerationDate() : ?\DateTime
{
return $this->backupCodesGenerationDate;
}
/**
* Return version for the trusted device token. Increase version to invalidate all trusted token of the user.
* @return int The version of trusted device token
*/
public function getTrustedTokenVersion(): int
{
return $this->trustedDeviceCookieVersion;
}
/**
* Invalidate all trusted device tokens at once, by incrementing the token version.
* You have to flush the changes to database afterwards.
*/
public function invalidateTrustedDeviceTokens() : void
{
$this->trustedDeviceCookieVersion++;
}
/**
* Check if U2F is enabled
* @return bool
*/
public function isU2FAuthEnabled(): bool
{
return count($this->u2fKeys) > 0;
}
/**
* Get all U2F Keys that are associated with this user
* @return Collection<TwoFactorKeyInterface>
*/
public function getU2FKeys(): Collection
{
return $this->u2fKeys;
}
/**
* Add a U2F key to this user.
* @param TwoFactorKeyInterface $key
*/
public function addU2FKey(TwoFactorKeyInterface $key): void
{
$this->u2fKeys->add($key);
}
/**
* Remove a U2F key from this user.
* @param TwoFactorKeyInterface $key
*/
public function removeU2FKey(TwoFactorKeyInterface $key): void
{
$this->u2fKeys->removeElement($key);
}
/**
* @inheritDoc
*/
public function getPreferredTwoFactorProvider(): ?string
{
//If U2F is available then prefer it
if($this->isU2FAuthEnabled()) {
return 'u2f_two_factor';
}
//Otherwise use other methods
return null;
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\EventSubscriber;
use App\Entity\UserSystem\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\HttpUtils;
/**
* This event subscriber redirects a user to its settings page, when it needs to change its password or is enforced
* to setup a 2FA method (enforcement can be set per group).
* In this cases the user is unable to access sites other than the whitelisted (see ALLOWED_ROUTES).
* @package App\EventSubscriber
*/
class PasswordChangeNeededSubscriber implements EventSubscriberInterface
{
protected $security;
protected $flashBag;
protected $httpUtils;
/**
* @var string[] The routes the user is allowed to access without being redirected.
* This should be only routes related to login/logout and user settings
*/
public const ALLOWED_ROUTES = [
'2fa_login',
'2fa_login_check',
'user_settings',
'club_base_register_u2f',
'logout',
];
/** @var string The route the user will redirected to, if he needs to change this password */
public const REDIRECT_TARGET = 'user_settings';
public function __construct(Security $security, FlashBagInterface $flashBag, HttpUtils $httpUtils)
{
$this->security = $security;
$this->flashBag = $flashBag;
$this->httpUtils = $httpUtils;
}
/**
* This function is called when the kernel encounters a request.
* It checks if the user must change its password or add an 2FA mehtod and redirect it to the user settings page,
* if needed.
* @param RequestEvent $event
*/
public function redirectToSettingsIfNeeded(RequestEvent $event) : void
{
$user = $this->security->getUser();
$request = $event->getRequest();
if(!$event->isMasterRequest()) {
return;
}
if(!$user instanceof User) {
return;
}
//Abort if we dont need to redirect the user.
if (!$user->isNeedPwChange() && !static::TFARedirectNeeded($user)) {
return;
}
//Check for a whitelisted URL
foreach (static::ALLOWED_ROUTES as $route) {
//Dont do anything if we encounter an allowed route
if ($this->httpUtils->checkRequestPath($request, $route)) {
return;
}
}
/* Dont redirect tree endpoints, as this would cause trouble and creates multiple flash
warnigs for one page reload */
if(strpos($request->getUri(), '/tree/') !== false) {
return;
}
//Show appropriate message to user about the reason he was redirected
if($user->isNeedPwChange()) {
$this->flashBag->add('warning', 'user.pw_change_needed.flash');
}
if(static::TFARedirectNeeded($user)) {
$this->flashBag->add('warning', 'user.2fa_needed.flash');
}
$event->setResponse($this->httpUtils->createRedirectResponse($request, static::REDIRECT_TARGET));
}
/**
* Check if a redirect because of a missing 2FA method is needed.
* That is the case if the group of the user enforces 2FA, but the user has neither Google Authenticator nor an
* U2F key setup.
* @param User $user The user for which should be checked if it needs to be redirected.
* @return bool True if the user needs to be redirected.
*/
public static function TFARedirectNeeded(User $user) : bool
{
$tfa_enabled = $user->isU2FAuthEnabled() || $user->isGoogleAuthenticatorEnabled();
if ($user->getGroup() !== null && $user->getGroup()->isEnforce2FA() && !$tfa_enabled) {
return true;
}
return false;
}
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => 'redirectToSettingsIfNeeded',
];
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\EventSubscriber;
use App\Entity\UserSystem\U2FKey;
use Doctrine\ORM\EntityManagerInterface;
use R\U2FTwoFactorBundle\Event\RegisterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class U2FRegistrationSubscriber implements EventSubscriberInterface
{
/** @var UrlGeneratorInterface */
private $router;
protected $em;
protected $demo_mode;
protected $flashBag;
public function __construct(UrlGeneratorInterface $router, EntityManagerInterface $entityManager, FlashBagInterface $flashBag, bool $demo_mode)
{
$this->router = $router;
$this->em = $entityManager;
$this->demo_mode = $demo_mode;
$this->flashBag = $flashBag;
}
/** @return string[] **/
public static function getSubscribedEvents(): array
{
return array(
'r_u2f_two_factor.register' => 'onRegister',
);
}
public function onRegister(RegisterEvent $event): void
{
//Skip adding of U2F key on demo mode
if (!$this->demo_mode) {
$user = $event->getUser();
$registration = $event->getRegistration();
$newKey = new U2FKey();
$newKey->fromRegistrationData($registration);
$newKey->setUser($user);
$newKey->setName($event->getKeyName());
// persist the new key
$this->em->persist($newKey);
$this->em->flush();
$this->flashBag->add('success', 'tfa_u2f.key_added_successful');
}
// generate new response, here we redirect the user to the fos user
// profile
$response = new RedirectResponse($this->router->generate('user_settings'));
$event->setResponse($response);
}
}

View file

@ -23,12 +23,22 @@ namespace App\Form\AdminPages;
use App\Entity\Base\NamedDBElement;
use App\Form\Permissions\PermissionsType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
class GroupAdminForm extends BaseEntityAdminForm
{
protected function additionalFormElements(FormBuilderInterface $builder, array $options, NamedDBElement $entity)
{
$is_new = null === $entity->getID();
$builder->add('enforce2FA', CheckboxType::class, ['required' => false,
'label' => 'group.edit.enforce_2fa',
'help' => 'entity.edit.enforce_2fa.help',
'label_attr' => ['class' => 'checkbox-custom'],
'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity)
]);
$builder->add('permissions', PermissionsType::class, [
'mapped' => false,
'data' => $builder->getData(),

View file

@ -0,0 +1,94 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Form;
use App\Entity\UserSystem\User;
use App\Validator\Constraints\ValidGoogleAuthCode;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\ResetType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class TFAGoogleSettingsType extends AbstractType
{
protected $translator;
public function __construct()
{
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
$form = $event->getForm();
/** @var User $user */
$user = $event->getData();
//Only show setup fields, when google authenticator is not enabled
if(!$user->isGoogleAuthenticatorEnabled()) {
$form->add(
'google_confirmation',
TextType::class,
[
'mapped' => false,
'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*', 'autocomplete' => 'off'],
'constraints' => [new ValidGoogleAuthCode()]
]
);
$form->add(
'googleAuthenticatorSecret',
HiddenType::class,
[
'disabled' => false,
]
);
$form->add('submit', SubmitType::class, [
'label' => 'tfa_google.enable'
]);
} else {
$form->add('submit', SubmitType::class, [
'label' =>'tfa_google.disable',
'attr' => ['class' => 'btn-danger']
]);
}
});
//$builder->add('cancel', ResetType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20191214153125 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE u2f_keys (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, key_handle VARCHAR(255) NOT NULL, public_key VARCHAR(255) NOT NULL, certificate LONGTEXT NOT NULL, counter VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_4F4ADB4BA76ED395 (user_id), UNIQUE INDEX user_unique (user_id, key_handle), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE u2f_keys ADD CONSTRAINT FK_4F4ADB4BA76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id)');
$this->addSql('ALTER TABLE `groups` ADD enforce_2fa TINYINT(1) NOT NULL');
$this->addSql('ALTER TABLE users ADD google_authenticator_secret VARCHAR(255) DEFAULT NULL, ADD backup_codes LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', ADD backup_codes_generation_date DATETIME DEFAULT NULL, ADD trusted_device_cookie_version INT NOT NULL');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE u2f_keys');
$this->addSql('ALTER TABLE `groups` DROP enforce_2fa');
$this->addSql('ALTER TABLE `users` DROP google_authenticator_secret, DROP backup_codes, DROP backup_codes_generation_date, DROP trusted_device_cookie_version');
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Services\TFA;
/**
* This class generates random backup codes for two factor authentication
* @package App\Services\TFA
*/
class BackupCodeGenerator
{
protected $code_length;
protected $code_count;
/**
* BackupCodeGenerator constructor.
* @param int $code_length How many characters a single code should have.
* @param int $code_count How many codes are generated for a whole backup set.
*/
public function __construct(int $code_length, int $code_count)
{
if ($code_length > 32) {
throw new \RuntimeException('Backup code can have maximum 32 digits!');
}
if ($code_length < 6) {
throw new \RuntimeException('Code must have at least 6 digits to ensure security!');
}
$this->code_count = $code_count;
$this->code_length = $code_length;
}
/**
* Generates a single backup code.
* It is a random hexadecimal value with the digit count configured in constructor
* @return string The generated backup code (e.g. 1f3870be2)
* @throws \Exception If no entropy source is available.
*/
public function generateSingleCode() : string
{
$bytes = random_bytes(32);
return substr(md5($bytes), 0, $this->code_length);
}
/**
* Returns a full backup code set. The code count can be configured in the constructor
* @return string[] An array containing different backup codes.
*/
public function generateCodeSet() : array
{
$array = [];
for($n=0; $n<$this->code_count; $n++) {
$array[] = $this->generateSingleCode();
}
return $array;
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Services\TFA;
use App\Entity\UserSystem\User;
/**
* This services offers methods to manage backup codes for two factor authentication
* @package App\Services\TFA
*/
class BackupCodeManager
{
protected $backupCodeGenerator;
public function __construct(BackupCodeGenerator $backupCodeGenerator)
{
$this->backupCodeGenerator = $backupCodeGenerator;
}
/**
* Enable backup codes for the given user, by generating a set of backup codes.
* If the backup codes were already enabled before, they a
* @param User $user
*/
public function enableBackupCodes(User $user)
{
if(empty($user->getBackupCodes())) {
$this->regenerateBackupCodes($user);
}
}
/**
* Disable (remove) the backup codes when no other 2 factor authentication methods are enabled.
* @param User $user
*/
public function disableBackupCodesIfUnused(User $user)
{
if($user->isGoogleAuthenticatorEnabled()) {
return;
}
$user->setBackupCodes([]);
}
/**
* Generates a new set of backup codes for the user. If no backup codes were available before, new ones are
* generated.
* @param User $user The user for which the backup codes should be regenerated
*/
public function regenerateBackupCodes(User $user)
{
$codes = $this->backupCodeGenerator->generateCodeSet();
$user->setBackupCodes($codes);
}
}

View file

@ -33,6 +33,7 @@ use App\Services\MoneyFormatter;
use App\Services\SIFormatter;
use App\Services\TreeBuilder;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
@ -49,13 +50,14 @@ class AppExtension extends AbstractExtension
protected $amountFormatter;
protected $attachmentURLGenerator;
protected $FAIconGenerator;
protected $translator;
public function __construct(EntityURLGenerator $entityURLGenerator, MarkdownParser $markdownParser,
SerializerInterface $serializer, TreeBuilder $treeBuilder,
MoneyFormatter $moneyFormatter,
SIFormatter $SIFormatter, AmountFormatter $amountFormatter,
AttachmentURLGenerator $attachmentURLGenerator,
FAIconGenerator $FAIconGenerator)
SerializerInterface $serializer, TreeBuilder $treeBuilder,
MoneyFormatter $moneyFormatter,
SIFormatter $SIFormatter, AmountFormatter $amountFormatter,
AttachmentURLGenerator $attachmentURLGenerator,
FAIconGenerator $FAIconGenerator, TranslatorInterface $translator)
{
$this->entityURLGenerator = $entityURLGenerator;
$this->markdownParser = $markdownParser;
@ -66,6 +68,7 @@ class AppExtension extends AbstractExtension
$this->amountFormatter = $amountFormatter;
$this->attachmentURLGenerator = $attachmentURLGenerator;
$this->FAIconGenerator = $FAIconGenerator;
$this->translator = $translator;
}
public function getFilters()

View file

@ -0,0 +1,30 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class ValidGoogleAuthCode extends Constraint
{
}

View file

@ -0,0 +1,83 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
namespace App\Validator\Constraints;
use App\Entity\UserSystem\User;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ValidGoogleAuthCodeValidator extends ConstraintValidator
{
protected $googleAuthenticator;
public function __construct(GoogleAuthenticator $googleAuthenticator)
{
$this->googleAuthenticator = $googleAuthenticator;
}
/**
* @inheritDoc
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof ValidGoogleAuthCode) {
throw new UnexpectedTypeException($constraint, ValidGoogleAuthCode::class);
}
if (null === $value || '' === $value) {
return;
}
if (!\is_string($value)) {
throw new UnexpectedValueException($value, 'string');
}
if(!ctype_digit($value)) {
$this->context->addViolation('validator.google_code.only_digits_allowed');
}
//Number must have 6 digits
if(strlen($value) !== 6) {
$this->context->addViolation('validator.google_code.wrong_digit_count');
}
//Try to retrieve the user we want to check
if($this->context->getObject() instanceof FormInterface &&
$this->context->getObject()->getParent() instanceof FormInterface
&& $this->context->getObject()->getParent()->getData() instanceof User) {
$user = $this->context->getObject()->getParent()->getData();
//Check if the given code is valid
if(!$this->googleAuthenticator->checkCode($user, $value)) {
$this->context->addViolation('validator.google_code.wrong_code');
}
}
}
}