Issues (30)

Service/U2fSecurity.php (2 issues)

Languages
Labels
Severity
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
$updatedKey of type stdClass is incompatible with the type Mbarbey\U2fSecurityBundl...del\Key\U2fKeyInterface expected by parameter $key of Mbarbey\U2fSecurityBundl...essEvent::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

205
            $this->dispatcher->dispatch(U2fAuthenticationSuccessEvent::getName(), new U2fAuthenticationSuccessEvent($user, /** @scrutinizer ignore-type */ $updatedKey));
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 ignore-type  annotation

211
            $this->dispatcher->dispatch(U2fPostAuthenticationEvent::getName(), new U2fPostAuthenticationEvent($user, /** @scrutinizer ignore-type */ $updatedKey));
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