diff options
author | Jeremy Benoist <jeremy.benoist@gmail.com> | 2018-12-02 12:43:05 +0100 |
---|---|---|
committer | Jeremy Benoist <jeremy.benoist@gmail.com> | 2019-01-23 13:28:02 +0100 |
commit | a6b242a1fd6f8900d80354361449f1bf62506ef9 (patch) | |
tree | f69d87208d0ebbdb8517529582280b174af74a16 /src/Wallabag/UserBundle | |
parent | acd4412080dfb73ecaa7f9983728d1d55bc27ea4 (diff) | |
download | wallabag-a6b242a1fd6f8900d80354361449f1bf62506ef9.tar.gz wallabag-a6b242a1fd6f8900d80354361449f1bf62506ef9.tar.zst wallabag-a6b242a1fd6f8900d80354361449f1bf62506ef9.zip |
Enable OTP 2FA
- Update SchebTwoFactorBundle to version 3
- Enable Google 2fa on the bundle
- Disallow ability to use both email and google as 2fa
- Update Ocramius Proxy Manager to handle typed function & attributes (from PHP 7)
- use `$this->addFlash` shortcut instead of `$this->get('session')->getFlashBag()->add`
- update admin to be able to create/reset the 2fa
Diffstat (limited to 'src/Wallabag/UserBundle')
6 files changed, 156 insertions, 47 deletions
diff --git a/src/Wallabag/UserBundle/Controller/ManageController.php b/src/Wallabag/UserBundle/Controller/ManageController.php index a9746fb4..08ed25dd 100644 --- a/src/Wallabag/UserBundle/Controller/ManageController.php +++ b/src/Wallabag/UserBundle/Controller/ManageController.php | |||
@@ -8,6 +8,7 @@ use Pagerfanta\Adapter\DoctrineORMAdapter; | |||
8 | use Pagerfanta\Exception\OutOfRangeCurrentPageException; | 8 | use Pagerfanta\Exception\OutOfRangeCurrentPageException; |
9 | use Pagerfanta\Pagerfanta; | 9 | use Pagerfanta\Pagerfanta; |
10 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; | 10 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
11 | use Symfony\Component\Form\FormInterface; | ||
11 | use Symfony\Component\HttpFoundation\Request; | 12 | use Symfony\Component\HttpFoundation\Request; |
12 | use Symfony\Component\Routing\Annotation\Route; | 13 | use Symfony\Component\Routing\Annotation\Route; |
13 | use Wallabag\UserBundle\Entity\User; | 14 | use Wallabag\UserBundle\Entity\User; |
@@ -31,10 +32,10 @@ class ManageController extends Controller | |||
31 | // enable created user by default | 32 | // enable created user by default |
32 | $user->setEnabled(true); | 33 | $user->setEnabled(true); |
33 | 34 | ||
34 | $form = $this->createForm('Wallabag\UserBundle\Form\NewUserType', $user); | 35 | $form = $this->createEditForm('NewUserType', $user, $request); |
35 | $form->handleRequest($request); | ||
36 | 36 | ||
37 | if ($form->isSubmitted() && $form->isValid()) { | 37 | if ($form->isSubmitted() && $form->isValid()) { |
38 | $user = $this->handleOtp($form, $user); | ||
38 | $userManager->updateUser($user); | 39 | $userManager->updateUser($user); |
39 | 40 | ||
40 | // dispatch a created event so the associated config will be created | 41 | // dispatch a created event so the associated config will be created |
@@ -62,14 +63,14 @@ class ManageController extends Controller | |||
62 | */ | 63 | */ |
63 | public function editAction(Request $request, User $user) | 64 | public function editAction(Request $request, User $user) |
64 | { | 65 | { |
66 | $userManager = $this->container->get('fos_user.user_manager'); | ||
67 | |||
65 | $deleteForm = $this->createDeleteForm($user); | 68 | $deleteForm = $this->createDeleteForm($user); |
66 | $editForm = $this->createForm('Wallabag\UserBundle\Form\UserType', $user); | 69 | $form = $this->createEditForm('UserType', $user, $request); |
67 | $editForm->handleRequest($request); | ||
68 | 70 | ||
69 | if ($editForm->isSubmitted() && $editForm->isValid()) { | 71 | if ($form->isSubmitted() && $form->isValid()) { |
70 | $em = $this->getDoctrine()->getManager(); | 72 | $user = $this->handleOtp($form, $user); |
71 | $em->persist($user); | 73 | $userManager->updateUser($user); |
72 | $em->flush(); | ||
73 | 74 | ||
74 | $this->get('session')->getFlashBag()->add( | 75 | $this->get('session')->getFlashBag()->add( |
75 | 'notice', | 76 | 'notice', |
@@ -81,7 +82,7 @@ class ManageController extends Controller | |||
81 | 82 | ||
82 | return $this->render('WallabagUserBundle:Manage:edit.html.twig', [ | 83 | return $this->render('WallabagUserBundle:Manage:edit.html.twig', [ |
83 | 'user' => $user, | 84 | 'user' => $user, |
84 | 'edit_form' => $editForm->createView(), | 85 | 'edit_form' => $form->createView(), |
85 | 'delete_form' => $deleteForm->createView(), | 86 | 'delete_form' => $deleteForm->createView(), |
86 | 'twofactor_auth' => $this->getParameter('twofactor_auth'), | 87 | 'twofactor_auth' => $this->getParameter('twofactor_auth'), |
87 | ]); | 88 | ]); |
@@ -157,7 +158,7 @@ class ManageController extends Controller | |||
157 | } | 158 | } |
158 | 159 | ||
159 | /** | 160 | /** |
160 | * Creates a form to delete a User entity. | 161 | * Create a form to delete a User entity. |
161 | * | 162 | * |
162 | * @param User $user The User entity | 163 | * @param User $user The User entity |
163 | * | 164 | * |
@@ -171,4 +172,50 @@ class ManageController extends Controller | |||
171 | ->getForm() | 172 | ->getForm() |
172 | ; | 173 | ; |
173 | } | 174 | } |
175 | |||
176 | /** | ||
177 | * Create a form to create or edit a User entity. | ||
178 | * | ||
179 | * @param string $type Might be NewUserType or UserType | ||
180 | * @param User $user The new / edit user | ||
181 | * @param Request $request The request | ||
182 | * | ||
183 | * @return FormInterface | ||
184 | */ | ||
185 | private function createEditForm($type, User $user, Request $request) | ||
186 | { | ||
187 | $form = $this->createForm('Wallabag\UserBundle\Form\\' . $type, $user); | ||
188 | $form->handleRequest($request); | ||
189 | |||
190 | // `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way | ||
191 | if (true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) { | ||
192 | $form->get('googleTwoFactor')->setData(true); | ||
193 | } | ||
194 | |||
195 | return $form; | ||
196 | } | ||
197 | |||
198 | /** | ||
199 | * Handle OTP update, taking care to only have one 2fa enable at a time. | ||
200 | * | ||
201 | * @see ConfigController | ||
202 | * | ||
203 | * @param FormInterface $form | ||
204 | * @param User $user | ||
205 | * | ||
206 | * @return User | ||
207 | */ | ||
208 | private function handleOtp(FormInterface $form, User $user) | ||
209 | { | ||
210 | if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) { | ||
211 | $user->setGoogleAuthenticatorSecret($this->get('scheb_two_factor.security.google_authenticator')->generateSecret()); | ||
212 | $user->setEmailTwoFactor(false); | ||
213 | |||
214 | return $user; | ||
215 | } | ||
216 | |||
217 | $user->setGoogleAuthenticatorSecret(null); | ||
218 | |||
219 | return $user; | ||
220 | } | ||
174 | } | 221 | } |
diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php index 48446e3c..6e305719 100644 --- a/src/Wallabag/UserBundle/Entity/User.php +++ b/src/Wallabag/UserBundle/Entity/User.php | |||
@@ -8,8 +8,8 @@ use FOS\UserBundle\Model\User as BaseUser; | |||
8 | use JMS\Serializer\Annotation\Accessor; | 8 | use JMS\Serializer\Annotation\Accessor; |
9 | use JMS\Serializer\Annotation\Groups; | 9 | use JMS\Serializer\Annotation\Groups; |
10 | use JMS\Serializer\Annotation\XmlRoot; | 10 | use JMS\Serializer\Annotation\XmlRoot; |
11 | use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface; | 11 | use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterface; |
12 | use Scheb\TwoFactorBundle\Model\TrustedComputerInterface; | 12 | use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface as GoogleTwoFactorInterface; |
13 | use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; | 13 | use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; |
14 | use Symfony\Component\Security\Core\User\UserInterface; | 14 | use Symfony\Component\Security\Core\User\UserInterface; |
15 | use Wallabag\ApiBundle\Entity\Client; | 15 | use Wallabag\ApiBundle\Entity\Client; |
@@ -28,7 +28,7 @@ use Wallabag\CoreBundle\Helper\EntityTimestampsTrait; | |||
28 | * @UniqueEntity("email") | 28 | * @UniqueEntity("email") |
29 | * @UniqueEntity("username") | 29 | * @UniqueEntity("username") |
30 | */ | 30 | */ |
31 | class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterface | 31 | class User extends BaseUser implements EmailTwoFactorInterface, GoogleTwoFactorInterface |
32 | { | 32 | { |
33 | use EntityTimestampsTrait; | 33 | use EntityTimestampsTrait; |
34 | 34 | ||
@@ -123,16 +123,16 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
123 | private $authCode; | 123 | private $authCode; |
124 | 124 | ||
125 | /** | 125 | /** |
126 | * @var bool | 126 | * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true) |
127 | * | ||
128 | * @ORM\Column(type="boolean") | ||
129 | */ | 127 | */ |
130 | private $twoFactorAuthentication = false; | 128 | private $googleAuthenticatorSecret; |
131 | 129 | ||
132 | /** | 130 | /** |
133 | * @ORM\Column(type="json_array", nullable=true) | 131 | * @var bool |
132 | * | ||
133 | * @ORM\Column(type="boolean") | ||
134 | */ | 134 | */ |
135 | private $trusted; | 135 | private $emailTwoFactor = false; |
136 | 136 | ||
137 | public function __construct() | 137 | public function __construct() |
138 | { | 138 | { |
@@ -233,49 +233,89 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf | |||
233 | /** | 233 | /** |
234 | * @return bool | 234 | * @return bool |
235 | */ | 235 | */ |
236 | public function isTwoFactorAuthentication() | 236 | public function isEmailTwoFactor() |
237 | { | ||
238 | return $this->emailTwoFactor; | ||
239 | } | ||
240 | |||
241 | /** | ||
242 | * @param bool $emailTwoFactor | ||
243 | */ | ||
244 | public function setEmailTwoFactor($emailTwoFactor) | ||
237 | { | 245 | { |
238 | return $this->twoFactorAuthentication; | 246 | $this->emailTwoFactor = $emailTwoFactor; |
239 | } | 247 | } |
240 | 248 | ||
241 | /** | 249 | /** |
242 | * @param bool $twoFactorAuthentication | 250 | * Used in the user config form to be "like" the email option. |
243 | */ | 251 | */ |
244 | public function setTwoFactorAuthentication($twoFactorAuthentication) | 252 | public function isGoogleTwoFactor() |
245 | { | 253 | { |
246 | $this->twoFactorAuthentication = $twoFactorAuthentication; | 254 | return $this->isGoogleAuthenticatorEnabled(); |
247 | } | 255 | } |
248 | 256 | ||
249 | public function isEmailAuthEnabled() | 257 | /** |
258 | * {@inheritdoc} | ||
259 | */ | ||
260 | public function isEmailAuthEnabled(): bool | ||
250 | { | 261 | { |
251 | return $this->twoFactorAuthentication; | 262 | return $this->emailTwoFactor; |
252 | } | 263 | } |
253 | 264 | ||
254 | public function getEmailAuthCode() | 265 | /** |
266 | * {@inheritdoc} | ||
267 | */ | ||
268 | public function getEmailAuthCode(): string | ||
255 | { | 269 | { |
256 | return $this->authCode; | 270 | return $this->authCode; |
257 | } | 271 | } |
258 | 272 | ||
259 | public function setEmailAuthCode($authCode) | 273 | /** |
274 | * {@inheritdoc} | ||
275 | */ | ||
276 | public function setEmailAuthCode(string $authCode): void | ||
260 | { | 277 | { |
261 | $this->authCode = $authCode; | 278 | $this->authCode = $authCode; |
262 | } | 279 | } |
263 | 280 | ||
264 | public function addTrustedComputer($token, \DateTime $validUntil) | 281 | /** |
282 | * {@inheritdoc} | ||
283 | */ | ||
284 | public function getEmailAuthRecipient(): string | ||
265 | { | 285 | { |
266 | $this->trusted[$token] = $validUntil->format('r'); | 286 | return $this->email; |
267 | } | 287 | } |
268 | 288 | ||
269 | public function isTrustedComputer($token) | 289 | /** |
290 | * {@inheritdoc} | ||
291 | */ | ||
292 | public function isGoogleAuthenticatorEnabled(): bool | ||
270 | { | 293 | { |
271 | if (isset($this->trusted[$token])) { | 294 | return $this->googleAuthenticatorSecret ? true : false; |
272 | $now = new \DateTime(); | 295 | } |
273 | $validUntil = new \DateTime($this->trusted[$token]); | ||
274 | 296 | ||
275 | return $now < $validUntil; | 297 | /** |
276 | } | 298 | * {@inheritdoc} |
299 | */ | ||
300 | public function getGoogleAuthenticatorUsername(): string | ||
301 | { | ||
302 | return $this->username; | ||
303 | } | ||
277 | 304 | ||
278 | return false; | 305 | /** |
306 | * {@inheritdoc} | ||
307 | */ | ||
308 | public function getGoogleAuthenticatorSecret(): string | ||
309 | { | ||
310 | return $this->googleAuthenticatorSecret; | ||
311 | } | ||
312 | |||
313 | /** | ||
314 | * {@inheritdoc} | ||
315 | */ | ||
316 | public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void | ||
317 | { | ||
318 | $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; | ||
279 | } | 319 | } |
280 | 320 | ||
281 | /** | 321 | /** |
diff --git a/src/Wallabag/UserBundle/Form/UserType.php b/src/Wallabag/UserBundle/Form/UserType.php index 56fea640..026db9a2 100644 --- a/src/Wallabag/UserBundle/Form/UserType.php +++ b/src/Wallabag/UserBundle/Form/UserType.php | |||
@@ -35,9 +35,14 @@ class UserType extends AbstractType | |||
35 | 'required' => false, | 35 | 'required' => false, |
36 | 'label' => 'user.form.enabled_label', | 36 | 'label' => 'user.form.enabled_label', |
37 | ]) | 37 | ]) |
38 | ->add('twoFactorAuthentication', CheckboxType::class, [ | 38 | ->add('emailTwoFactor', CheckboxType::class, [ |
39 | 'required' => false, | 39 | 'required' => false, |
40 | 'label' => 'user.form.twofactor_label', | 40 | 'label' => 'user.form.twofactor_email_label', |
41 | ]) | ||
42 | ->add('googleTwoFactor', CheckboxType::class, [ | ||
43 | 'required' => false, | ||
44 | 'label' => 'user.form.twofactor_google_label', | ||
45 | 'mapped' => false, | ||
41 | ]) | 46 | ]) |
42 | ->add('save', SubmitType::class, [ | 47 | ->add('save', SubmitType::class, [ |
43 | 'label' => 'user.form.save', | 48 | 'label' => 'user.form.save', |
diff --git a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php index aed805c9..e8e29aa9 100644 --- a/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php +++ b/src/Wallabag/UserBundle/Mailer/AuthCodeMailer.php | |||
@@ -78,7 +78,7 @@ class AuthCodeMailer implements AuthCodeMailerInterface | |||
78 | * | 78 | * |
79 | * @param TwoFactorInterface $user | 79 | * @param TwoFactorInterface $user |
80 | */ | 80 | */ |
81 | public function sendAuthCode(TwoFactorInterface $user) | 81 | public function sendAuthCode(TwoFactorInterface $user): void |
82 | { | 82 | { |
83 | $template = $this->twig->loadTemplate('WallabagUserBundle:TwoFactor:email_auth_code.html.twig'); | 83 | $template = $this->twig->loadTemplate('WallabagUserBundle:TwoFactor:email_auth_code.html.twig'); |
84 | 84 | ||
diff --git a/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig b/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig index c8471bdd..47a5cb78 100644 --- a/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Authentication/form.html.twig | |||
@@ -1,7 +1,8 @@ | |||
1 | {# Override `vendor/scheb/two-factor-bundle/Resources/views/Authentication/form.html.twig` #} | ||
1 | {% extends "WallabagUserBundle::layout.html.twig" %} | 2 | {% extends "WallabagUserBundle::layout.html.twig" %} |
2 | 3 | ||
3 | {% block fos_user_content %} | 4 | {% block fos_user_content %} |
4 | <form class="form" action="" method="post"> | 5 | <form class="form" action="{{ path("2fa_login_check") }}" method="post"> |
5 | <div class="card-content"> | 6 | <div class="card-content"> |
6 | <div class="row"> | 7 | <div class="row"> |
7 | 8 | ||
@@ -9,14 +10,19 @@ | |||
9 | <p class="error">{{ flashMessage|trans }}</p> | 10 | <p class="error">{{ flashMessage|trans }}</p> |
10 | {% endfor %} | 11 | {% endfor %} |
11 | 12 | ||
13 | {# Authentication errors #} | ||
14 | {% if authenticationError %} | ||
15 | <p class="error">{{ authenticationError|trans(authenticationErrorData) }}</p> | ||
16 | {% endif %} | ||
17 | |||
12 | <div class="input-field col s12"> | 18 | <div class="input-field col s12"> |
13 | <label for="_auth_code">{{ "scheb_two_factor.auth_code"|trans }}</label> | 19 | <label for="_auth_code">{{ "scheb_two_factor.auth_code"|trans }}</label> |
14 | <input id="_auth_code" type="text" autocomplete="off" name="_auth_code" /> | 20 | <input id="_auth_code" type="text" autocomplete="off" name="{{ authCodeParameterName }}" /> |
15 | </div> | 21 | </div> |
16 | 22 | ||
17 | {% if useTrustedOption %} | 23 | {% if displayTrustedOption %} |
18 | <div class="input-field col s12"> | 24 | <div class="input-field col s12"> |
19 | <input id="_trusted" type="checkbox" name="_trusted" /> | 25 | <input id="_trusted" type="checkbox" name="{{ trustedParameterName }}" /> |
20 | <label for="_trusted">{{ "scheb_two_factor.trusted"|trans }}</label> | 26 | <label for="_trusted">{{ "scheb_two_factor.trusted"|trans }}</label> |
21 | </div> | 27 | </div> |
22 | {% endif %} | 28 | {% endif %} |
diff --git a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig index 3ffd15f5..8be37e79 100644 --- a/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig +++ b/src/Wallabag/UserBundle/Resources/views/Manage/edit.html.twig | |||
@@ -50,10 +50,21 @@ | |||
50 | {% if twofactor_auth %} | 50 | {% if twofactor_auth %} |
51 | <div class="row"> | 51 | <div class="row"> |
52 | <div class="input-field col s12"> | 52 | <div class="input-field col s12"> |
53 | {{ form_widget(edit_form.twoFactorAuthentication) }} | 53 | {{ form_widget(edit_form.emailTwoFactor) }} |
54 | {{ form_label(edit_form.twoFactorAuthentication) }} | 54 | {{ form_label(edit_form.emailTwoFactor) }} |
55 | {{ form_errors(edit_form.twoFactorAuthentication) }} | 55 | {{ form_errors(edit_form.emailTwoFactor) }} |
56 | </div> | 56 | </div> |
57 | <div class="input-field col s12"> | ||
58 | {{ form_widget(edit_form.googleTwoFactor) }} | ||
59 | {{ form_label(edit_form.googleTwoFactor) }} | ||
60 | {{ form_errors(edit_form.googleTwoFactor) }} | ||
61 | </div> | ||
62 | |||
63 | {% if user.isGoogleAuthenticatorEnabled %} | ||
64 | <div class="input-field col s12"> | ||
65 | <p><strong>OTP Secret</strong>: {{ user.googleAuthenticatorSecret }}</p> | ||
66 | </div> | ||
67 | {% endif %} | ||
57 | </div> | 68 | </div> |
58 | {% endif %} | 69 | {% endif %} |
59 | 70 | ||