1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * This file is part of the U2F Security bundle. |
||||
5 | * |
||||
6 | * (c) Michael Barbey <[email protected]> |
||||
7 | * |
||||
8 | * For the full copyright and license information, please view the LICENSE |
||||
9 | * file that was distributed with this source code. |
||||
10 | */ |
||||
11 | |||||
12 | namespace Mbarbey\U2fSecurityBundle\Service; |
||||
13 | |||||
14 | use Symfony\Component\HttpFoundation\Session\SessionInterface; |
||||
15 | use Samyoul\U2F\U2FServer\U2FServer; |
||||
16 | use Samyoul\U2F\U2FServer\U2FException; |
||||
17 | use Mbarbey\U2fSecurityBundle\Model\U2fRegistration\U2fRegistrationInterface; |
||||
18 | use Mbarbey\U2fSecurityBundle\Model\User\U2fUserInterface; |
||||
19 | use Mbarbey\U2fSecurityBundle\Model\U2fAuthentication\U2fAuthenticationInterface; |
||||
20 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
||||
21 | use Mbarbey\U2fSecurityBundle\Event\Authentication\U2fAuthenticationSuccessEvent; |
||||
22 | use Mbarbey\U2fSecurityBundle\Event\Registration\U2fPreRegistrationEvent; |
||||
23 | use Mbarbey\U2fSecurityBundle\Event\Registration\U2fRegistrationSuccessEvent; |
||||
24 | use Mbarbey\U2fSecurityBundle\Event\Registration\U2fPostRegistrationEvent; |
||||
25 | use Mbarbey\U2fSecurityBundle\Event\Registration\U2fRegistrationFailureEvent; |
||||
26 | use Mbarbey\U2fSecurityBundle\Event\Authentication\U2fPreAuthenticationEvent; |
||||
27 | use Mbarbey\U2fSecurityBundle\Event\Authentication\U2fAuthenticationFailureEvent; |
||||
28 | use Mbarbey\U2fSecurityBundle\Event\Authentication\U2fPostAuthenticationEvent; |
||||
29 | use Mbarbey\U2fSecurityBundle\Model\Key\U2fKeyInterface; |
||||
30 | use Mbarbey\U2fSecurityBundle\EventSubscriber\U2fSubscriber; |
||||
31 | |||||
32 | /** |
||||
33 | * U2F security engine |
||||
34 | * |
||||
35 | * This class allow to manage U2F exchanges by using the following structure : |
||||
36 | * - Create a registration request |
||||
37 | * - Validate a registration response |
||||
38 | * - Create an authentication request |
||||
39 | * - Validate an authentication response |
||||
40 | * |
||||
41 | * @author Michael Barbey <[email protected]> |
||||
42 | */ |
||||
43 | class U2fSecurity |
||||
44 | { |
||||
45 | private $session; |
||||
46 | private $dispatcher; |
||||
47 | |||||
48 | /** |
||||
49 | * @param SessionInterface $session The session manager |
||||
50 | * @param EventDispatcherInterface $dispatcher The event dispatcher |
||||
51 | */ |
||||
52 | public function __construct(SessionInterface $session, EventDispatcherInterface $dispatcher) |
||||
53 | { |
||||
54 | $this->session = $session; |
||||
55 | $this->dispatcher = $dispatcher; |
||||
56 | } |
||||
57 | |||||
58 | /** |
||||
59 | * If you want to add an extra layer of personalization to your system, you can call this function from your controller. |
||||
60 | * This function will send a U2fPreRegistrationEvent and check if any event listener want to cancel the current |
||||
61 | * registration process. |
||||
62 | * |
||||
63 | * It will return the dispatched event, so you can check if you should abort the registration or not. |
||||
64 | * |
||||
65 | * @param U2fUserInterface $user The user which want to register a security key |
||||
66 | * @param string $appId The appId (must be the HTTP protocol and your domain name (ex: https://example.com) |
||||
67 | * @return U2fPreRegistrationEvent The event dispathed and updated by event listeners |
||||
68 | */ |
||||
69 | public function canRegister(U2fUserInterface $user, $appId) |
||||
70 | { |
||||
71 | $event = new U2fPreRegistrationEvent($appId, $user); |
||||
72 | |||||
73 | if ($this->dispatcher->hasListeners($event->getName())) { |
||||
74 | $this->dispatcher->dispatch($event::getName(), $event); |
||||
75 | } |
||||
76 | |||||
77 | return $event; |
||||
78 | } |
||||
79 | |||||
80 | /** |
||||
81 | * Create a registration request array which will be used to communicate with the U2F security key. |
||||
82 | * |
||||
83 | * @param string $appId The appId (must be the HTTP protocol and your domain name (ex: https://example.com) |
||||
84 | * @return array The array containing all the data required for the authentication |
||||
85 | */ |
||||
86 | public function createRegistration($appId) |
||||
87 | { |
||||
88 | $registrationData = U2FServer::makeRegistration($appId); |
||||
89 | $this->session->set('registrationRequest', $registrationData['request']); |
||||
90 | |||||
91 | return ['request' => $registrationData['request'], 'signatures' => json_encode($registrationData['signatures'])]; |
||||
92 | } |
||||
93 | |||||
94 | /** |
||||
95 | * Validate the given U2F registration response and fill the given key with the correct data. |
||||
96 | * |
||||
97 | * @param U2fUserInterface $user The user which want to register a security key |
||||
98 | * @param U2fRegistrationInterface $registration The registration response |
||||
99 | * @param U2fKeyInterface $key The security key to fill with valid data |
||||
100 | * |
||||
101 | * @throws \Exception If there is an error, an exception is thrown to explain what went wrong |
||||
102 | */ |
||||
103 | public function validateRegistration(U2fUserInterface $user, U2fRegistrationInterface $registration, U2fKeyInterface $key) |
||||
104 | { |
||||
105 | $u2fRequest = $this->session->get('registrationRequest'); |
||||
106 | $u2fResponse = (object)json_decode($registration->getResponse(), true); |
||||
107 | |||||
108 | try { |
||||
109 | /* |
||||
110 | * Check if the response is correct |
||||
111 | */ |
||||
112 | $validatedRegistration = U2FServer::register($u2fRequest, $u2fResponse); |
||||
113 | |||||
114 | /* |
||||
115 | * Check if the key hasn't already been registered |
||||
116 | */ |
||||
117 | foreach ($user->getU2fKeys() as $existingKey) { |
||||
118 | if ($existingKey->getCertificate() == $validatedRegistration->getCertificate()) { |
||||
119 | throw new U2FException('Key already registered', 4); |
||||
120 | } |
||||
121 | } |
||||
122 | } catch (\Exception $e) { |
||||
123 | /* |
||||
124 | * In case of error, dispatch a failure-registration event and a post-registration event |
||||
125 | */ |
||||
126 | if ($this->dispatcher->hasListeners(U2fRegistrationFailureEvent::getName())) { |
||||
127 | $this->dispatcher->dispatch(U2fRegistrationFailureEvent::getName(), new U2fRegistrationFailureEvent($user, $e)); |
||||
128 | } |
||||
129 | if ($this->dispatcher->hasListeners(U2fPostRegistrationEvent::getName())) { |
||||
130 | $this->dispatcher->dispatch(U2fPostRegistrationEvent::getName(), new U2fPostRegistrationEvent($user)); |
||||
131 | } |
||||
132 | throw $e; |
||||
133 | } |
||||
134 | |||||
135 | /* |
||||
136 | * If everything went good, we fill the given key with correct data and dispatch both a success-registration event |
||||
137 | * and a post-registration event |
||||
138 | */ |
||||
139 | $key->setCertificate($validatedRegistration->getCertificate()); |
||||
140 | $key->setCounter((int)$validatedRegistration->getCounter()); |
||||
141 | $key->setKeyHandle($validatedRegistration->getKeyHandle()); |
||||
142 | $key->setPublicKey($validatedRegistration->getPublicKey()); |
||||
143 | |||||
144 | if ($this->dispatcher->hasListeners(U2fRegistrationSuccessEvent::getName())) { |
||||
145 | $this->dispatcher->dispatch(U2fRegistrationSuccessEvent::getName(), new U2fRegistrationSuccessEvent($user, $key)); |
||||
146 | } |
||||
147 | |||||
148 | $this->session->remove('registrationRequest'); |
||||
149 | |||||
150 | if ($this->dispatcher->hasListeners(U2fPostRegistrationEvent::getName())) { |
||||
151 | $this->dispatcher->dispatch(U2fPostRegistrationEvent::getName(), new U2fPostRegistrationEvent($user, $key)); |
||||
152 | } |
||||
153 | } |
||||
154 | |||||
155 | public function canAuthenticate($appId, U2fUserInterface $user) |
||||
156 | { |
||||
157 | $event = new U2fPreAuthenticationEvent($appId, $user); |
||||
158 | |||||
159 | if ($this->dispatcher->hasListeners($event->getName())) { |
||||
160 | $this->dispatcher->dispatch($event->getName(), $event); |
||||
161 | } |
||||
162 | |||||
163 | if ($event->isAborted()) { |
||||
164 | $this->session->remove('authenticationRequest'); |
||||
165 | } |
||||
166 | |||||
167 | return $event; |
||||
168 | } |
||||
169 | |||||
170 | public function createAuthentication($appId, U2fUserInterface $user) |
||||
171 | { |
||||
172 | $authenticationRequest = U2FServer::makeAuthentication($user->getU2fKeys()->toArray(), $appId); |
||||
173 | $this->session->set('authenticationRequest', $authenticationRequest); |
||||
174 | |||||
175 | return [ |
||||
176 | 'appId' => $appId, |
||||
177 | 'version' => U2FServer::VERSION, |
||||
178 | 'challenge' => $authenticationRequest[0]->challenge(), |
||||
179 | 'registeredKeys' => json_encode($authenticationRequest) |
||||
180 | ]; |
||||
181 | } |
||||
182 | |||||
183 | public function validateAuthentication(U2fUserInterface $user, U2fAuthenticationInterface $authentication) |
||||
184 | { |
||||
185 | try { |
||||
186 | $updatedKey = U2FServer::authenticate( |
||||
187 | $this->session->get('authenticationRequest'), |
||||
188 | $user->getU2fKeys()->toArray(), |
||||
189 | json_decode($authentication->getResponse()) |
||||
190 | ); |
||||
191 | } catch (\Exception $e) { |
||||
192 | if ($this->dispatcher->hasListeners(U2fAuthenticationFailureEvent::getName())) { |
||||
193 | $counter = $this->session->get('u2f_registration_error_counter', 0) +1; |
||||
194 | $this->session->set('u2f_registration_error_counter', $counter); |
||||
195 | $this->dispatcher->dispatch(U2fAuthenticationFailureEvent::getName(), new U2fAuthenticationFailureEvent($user, $e, $counter)); |
||||
196 | } |
||||
197 | if ($this->dispatcher->hasListeners(U2fPostAuthenticationEvent::getName())) { |
||||
198 | $this->dispatcher->dispatch(U2fPostAuthenticationEvent::getName(), new U2fPostAuthenticationEvent($user)); |
||||
199 | } |
||||
200 | |||||
201 | throw $e; |
||||
202 | } |
||||
203 | |||||
204 | if ($this->dispatcher->hasListeners(U2fAuthenticationSuccessEvent::getName())) { |
||||
205 | $this->dispatcher->dispatch(U2fAuthenticationSuccessEvent::getName(), new U2fAuthenticationSuccessEvent($user, $updatedKey)); |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
206 | } |
||||
207 | |||||
208 | $this->stopRequestingAuthentication(); |
||||
209 | |||||
210 | if ($this->dispatcher->hasListeners(U2fPostAuthenticationEvent::getName())) { |
||||
211 | $this->dispatcher->dispatch(U2fPostAuthenticationEvent::getName(), new U2fPostAuthenticationEvent($user, $updatedKey)); |
||||
0 ignored issues
–
show
$updatedKey of type stdClass is incompatible with the type Mbarbey\U2fSecurityBundl...ey\U2fKeyInterface|null expected by parameter $key of Mbarbey\U2fSecurityBundl...ionEvent::__construct() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
212 | } |
||||
213 | |||||
214 | return $updatedKey; |
||||
215 | } |
||||
216 | |||||
217 | public function stopRequestingAuthentication() |
||||
218 | { |
||||
219 | if ($this->session->has(U2fSubscriber::U2F_SECURITY_KEY)) { |
||||
220 | $this->session->remove(U2fSubscriber::U2F_SECURITY_KEY); |
||||
221 | } |
||||
222 | |||||
223 | $this->session->remove('authenticationRequest'); |
||||
224 | |||||
225 | if ($this->session->has('u2f_registration_error_counter')) { |
||||
226 | $this->session->remove('u2f_registration_error_counter'); |
||||
227 | } |
||||
228 | } |
||||
229 | } |
||||
230 |