Db.php 6.79 KB
Newer Older
1
<?php
2

3
4
5
namespace UnicaenAuth\Authentication\Adapter;

use UnicaenAuth\Options\ModuleOptions;
6
use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait;
7
8
use Zend\Authentication\Result as AuthenticationResult;
use Zend\Crypt\Password\Bcrypt;
9
use Zend\EventManager\EventInterface;
10
use Zend\Session\Container as SessionContainer;
11
use ZfcUser\Authentication\Adapter\AdapterChainEvent;
12
13
use ZfcUser\Entity\UserInterface;
use ZfcUser\Mapper\UserInterface as UserMapperInterface;
14
15

/**
Bertrand Gauthier's avatar
Bertrand Gauthier committed
16
17
18
 * Adpater d'authentification à partir de la base de données.
 * 
 * Ajout par rapport à la classe mère : si aucune base de données ou table n'existe,
19
 * l'authentification ne plante pas (i.e. renvoit false).
20
21
22
 *
 * @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr>
 */
23
class Db extends AbstractAdapter
24
{
25
26
    use ModuleOptionsAwareTrait;

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
    const TYPE = 'db';

    /**
     * @var string
     */
    protected $type = self::TYPE;

    /**
     * @var UserMapperInterface
     */
    protected $mapper;

    /**
     * @var callable
     */
    protected $credentialPreprocessor;

    /**
45
     * @var ModuleOptions
46
47
48
49
     */
    protected $options;

    /**
50
     * @inheritDoc
51
     */
52
    public function authenticate(EventInterface $e): bool
53
    {
54
55
56
        // NB: Dans la version 3.0.0 de zf-commons/zfc-user, cette méthode prend un EventInterface.
        // Mais dans la branche 3.x, c'est un AdapterChainEvent !
        // Si un jour c'est un AdapterChainEvent qui est attendu, plus besoin de faire $e->getTarget().
57
58
59
        $event = $e->getTarget(); /* @var $event AdapterChainEvent */

        if ($event->getIdentity()) {
60
            return true;
61
        }
62
63
64

        if ($this->isSatisfied()) {
            $storage = $this->getStorage()->read();
65
66
            $event
                ->setIdentity($storage['identity'])
67
68
                ->setCode(AuthenticationResult::SUCCESS)
                ->setMessages(array('Authentication successful.'));
69
            return true;
70
71
        }

72
        if (! $this->isEnabled()) {
73
            return false;
74
75
        }

76
77
        $identity   = $event->getRequest()->getPost()->get('identity');
        $credential = $event->getRequest()->getPost()->get('credential');
78
79
80
81
82
        $credential = $this->preProcessCredential($credential);
        /** @var UserInterface|null $userObject */
        $userObject = null;

        // Cycle through the configured identity sources and test each
83
        $fields = $this->options->getAuthIdentityFields();
84
85
86
87
        while (!is_object($userObject) && count($fields) > 0) {
            $mode = array_shift($fields);
            switch ($mode) {
                case 'username':
88
                    $userObject = $this->mapper->findByUsername($identity);
89
90
                    break;
                case 'email':
91
                    $userObject = $this->mapper->findByEmail($identity);
92
93
                    break;
            }
94
        }
95
96

        if (!$userObject) {
97
98
            $event
                ->setCode(AuthenticationResult::FAILURE_IDENTITY_NOT_FOUND)
99
100
                ->setMessages(array('A record with the supplied identity could not be found.'));
            $this->setSatisfied(false);
101
102
            return false;
        }
103

104
        if ($this->options->getEnableUserState()) {
105
            // Don't allow user to login if state is not in allowed list
106
107
108
            if (!in_array($userObject->getState(), $this->options->getAllowedLoginStates())) {
                $event
                    ->setCode(AuthenticationResult::FAILURE_UNCATEGORIZED)
109
110
111
112
113
114
115
                    ->setMessages(array('A record with the supplied identity is not active.'));
                $this->setSatisfied(false);
                return false;
            }
        }

        $bcrypt = new Bcrypt();
116
        $bcrypt->setCost($this->options->getPasswordCost());
117
118
        if (!$bcrypt->verify($credential, $userObject->getPassword())) {
            // Password does not match
119
120
            $event
                ->setCode(AuthenticationResult::FAILURE_CREDENTIAL_INVALID)
121
122
123
124
125
126
                ->setMessages(array('Supplied credential is invalid.'));
            $this->setSatisfied(false);
            return false;
        }

        // regen the id
127
        $session = new SessionContainer($this->getStorage()->getNamespace());
128
129
130
        $session->getManager()->regenerateId();

        // Success!
131
132
        $identity = $this->createSessionIdentity($userObject->getUsername());
        $event->setIdentity($identity);
133
134
135
136
        // Update user's password hash if the cost parameter has changed
        $this->updateUserPasswordHash($userObject, $credential, $bcrypt);
        $this->setSatisfied(true);
        $storage = $this->getStorage()->read();
137
        $storage['identity'] = $event->getIdentity();
138
        $this->getStorage()->write($storage);
139
140
        $event
            ->setCode(AuthenticationResult::SUCCESS)
141
            ->setMessages(array('Authentication successful.'));
142
143

        return true;
144
145
    }

146
147
148
    /**
     * @return bool
     */
149
    protected function isEnabled(): bool
150
151
152
153
154
155
156
157
158
159
    {
        $config = $this->moduleOptions->getDb();

        if (isset($config['enabled'])) {
            return (bool) $config['enabled'];
        }

        return false;
    }

160
    protected function updateUserPasswordHash(UserInterface $userObject, $password, Bcrypt $bcrypt): self
161
162
163
    {
        $hash = explode('$', $userObject->getPassword());
        if ($hash[2] === $bcrypt->getCost()) {
164
            return $this;
165
166
        }
        $userObject->setPassword($bcrypt->create($password));
167
168
        $this->mapper->update($userObject);

169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
        return $this;
    }

    public function preProcessCredential($credential)
    {
        $processor = $this->getCredentialPreprocessor();
        if (is_callable($processor)) {
            return $processor($credential);
        }

        return $credential;
    }

    /**
     * setMapper
     *
     * @param UserMapperInterface $mapper
186
     * @return self
187
     */
188
    public function setMapper(UserMapperInterface $mapper): self
189
190
191
192
193
194
195
196
197
    {
        $this->mapper = $mapper;

        return $this;
    }

    /**
     * Get credentialPreprocessor.
     *
198
     * @return callable|null
199
     */
200
    public function getCredentialPreprocessor(): ?callable
201
202
203
204
205
206
207
208
    {
        return $this->credentialPreprocessor;
    }

    /**
     * Set credentialPreprocessor.
     *
     * @param callable $credentialPreprocessor
209
     * @return self
210
     */
211
    public function setCredentialPreprocessor(callable $credentialPreprocessor): self
212
213
214
215
216
    {
        $this->credentialPreprocessor = $credentialPreprocessor;
        return $this;
    }

217
    /**
218
     * @param \ZfcUser\Options\ModuleOptions $options
219
220
     * @return self
     */
221
    public function setOptions(\ZfcUser\Options\ModuleOptions $options): self
222
223
224
225
226
227
228
229
    {
        $this->options = $options;
        return $this;
    }

    /**
     * @return ModuleOptions
     */
230
    protected function getOptions(): ModuleOptions
231
232
233
234
    {
        return $this->options;
    }
}