mbarbey /
u2f-security-bundle
| 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
Loading history...
|
|||||
| 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
Loading history...
|
|||||
| 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 |