Ldap.php 9.72 KB
Newer Older
Bertrand Gauthier's avatar
Bertrand Gauthier committed
1
<?php
2

Bertrand Gauthier's avatar
Bertrand Gauthier committed
3
4
namespace UnicaenAuth\Authentication\Adapter;

5
6
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Mapper\Ldap\People as LdapPeopleMapper;
7
use UnicaenApp\Options\ModuleOptions;
8
use UnicaenAuth\Options\Traits\ModuleOptionsAwareTrait;
9
use UnicaenAuth\Service\User;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
10
11
12
13
14
15
16
17
use Laminas\Authentication\Adapter\Ldap as LdapAuthAdapter;
use Laminas\Authentication\Exception\ExceptionInterface;
use Laminas\Authentication\Result as AuthenticationResult;
use Laminas\EventManager\Event;
use Laminas\EventManager\EventInterface;
use Laminas\EventManager\EventManager;
use Laminas\EventManager\EventManagerAwareInterface;
use Laminas\EventManager\EventManagerInterface;
18
use ZfcUser\Authentication\Adapter\AdapterChainEvent;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
19
20
21
22
23
24

/**
 * LDAP authentication adpater
 *
 * @author Bertrand GAUTHIER <bertrand.gauthier@unicaen.fr>
 */
25
class Ldap extends AbstractAdapter implements EventManagerAwareInterface
Bertrand Gauthier's avatar
Bertrand Gauthier committed
26
{
27
    use ModuleOptionsAwareTrait;
28

29
30
    const TYPE = 'ldap';

31
    const USURPATION_USERNAMES_SEP = '=';
Bertrand Gauthier's avatar
Bertrand Gauthier committed
32

33
34
    const LDAP_AUTHENTIFICATION_FAIL = 'authentification.ldap.fail';

35
36
37
38
39
    /**
     * @var string
     */
    protected $type = self::TYPE;

Bertrand Gauthier's avatar
Bertrand Gauthier committed
40
    /**
Bertrand Gauthier's avatar
Bertrand Gauthier committed
41
     * @var EventManager
Bertrand Gauthier's avatar
Bertrand Gauthier committed
42
43
     */
    protected $eventManager;
44

Bertrand Gauthier's avatar
Bertrand Gauthier committed
45
    /**
Bertrand Gauthier's avatar
Bertrand Gauthier committed
46
     * @var LdapAuthAdapter
Bertrand Gauthier's avatar
Bertrand Gauthier committed
47
     */
Bertrand Gauthier's avatar
Bertrand Gauthier committed
48
    protected $ldapAuthAdapter;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
49

50
51
52
53
54
    /**
     * @var LdapPeopleMapper
     */
    protected $ldapPeopleMapper;

55
    /**
56
     * @var string
57
58
     */
    protected $usernameUsurpe;
59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
    /**
     * @var User
     */
    private $userService;

    /**
     * @param User $userService
     */
    public function setUserService(User $userService)
    {
        $this->userService = $userService;
    }

    /**
74
     * @var ModuleOptions
75
76
77
78
     */
    private $appModuleOptions;

    /**
79
     * @param ModuleOptions $appModuleOptions
80
     */
81
    public function setAppModuleOptions(ModuleOptions $appModuleOptions)
82
83
84
85
    {
        $this->appModuleOptions = $appModuleOptions;
    }

Bertrand Gauthier's avatar
Bertrand Gauthier committed
86
    /**
87
     * @inheritDoc
Bertrand Gauthier's avatar
Bertrand Gauthier committed
88
     */
89
    public function authenticate(EventInterface $e): bool
Bertrand Gauthier's avatar
Bertrand Gauthier committed
90
    {
91
92
93
        // 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().
94
95
        $event = $e->getTarget();
        /* @var $event AdapterChainEvent */
96

Bertrand Gauthier's avatar
Bertrand Gauthier committed
97
        if ($this->isSatisfied()) {
98
99
            try {
                $storage = $this->getStorage()->read();
100
            } catch (ExceptionInterface $event) {
101
102
                throw new RuntimeException("Erreur de lecture du storage");
            }
103
104
            $event
                ->setIdentity($storage['identity'])
105
106
                ->setCode(AuthenticationResult::SUCCESS)
                ->setMessages(['Authentication successful.']);
107
            return true;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
108
109
        }

110
        $username = $event->getRequest()->getPost()->get('identity');
111
        $credential = $event->getRequest()->getPost()->get('credential');
Bertrand Gauthier's avatar
Bertrand Gauthier committed
112

113
114
115
116
117
118
        if (function_exists('mb_strtolower')) {
            $username = mb_strtolower($username, 'UTF-8');
        } else {
            $username = strtolower($username);
        }

119
        $success = $this->authenticateUsername($username, $credential);
120

Bertrand Gauthier's avatar
Bertrand Gauthier committed
121
        // Failure!
122
        if (!$success) {
123
124
            $event
                ->setCode(AuthenticationResult::FAILURE)
125
                ->setMessages([/*'LDAP bind failed.'*/]);
126
127
128
129
130
            $this->setSatisfied(false);
            return false;
        }

        // recherche de l'individu dans l'annuaire LDAP
131
        $ldapPeople = $this->getLdapPeopleMapper()->findOneByUsername($this->usernameUsurpe ?: $username);
132
        if (!$ldapPeople) {
133
            $event
134
                ->setCode(AuthenticationResult::FAILURE)
135
                ->setMessages([/*'Authentication failed.'*/]);
Bertrand Gauthier's avatar
Bertrand Gauthier committed
136
137
138
            $this->setSatisfied(false);
            return false;
        }
139

140
141
142
        $identity = $this->createSessionIdentity($this->usernameUsurpe ?: $username);

        $event->setIdentity($identity);
Bertrand Gauthier's avatar
Bertrand Gauthier committed
143
        $this->setSatisfied(true);
144
145
        try {
            $storage = $this->getStorage()->read();
146
            $storage['identity'] = $event->getIdentity();
147
            $this->getStorage()->write($storage);
148
        } catch (ExceptionInterface $event) {
149
150
            throw new RuntimeException("Erreur de concernant le storage");
        }
151
152
        $event
            ->setCode(AuthenticationResult::SUCCESS)
153
            ->setMessages(['Authentication successful.']);
154

155
        /* @var $userService User */
156
        $this->userService->userAuthenticated($ldapPeople);
157
158

        return true;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
159
160
    }

161
162
163
164
165
166
167
168
169
    /**
     * Extrait le loginUsurpateur et le loginUsurpé si l'identifiant spécifé est de la forme
     * "loginUsurpateur=loginUsurpé".
     *
     * @param string $identifiant Identifiant, éventuellement de la forme "loginUsurpateur=loginUsurpé"
     * @return array
     * [loginUsurpateur, loginUsurpé] si l'identifiant est de la forme "loginUsurpateur=loginUsurpé" ;
     * [] sinon.
     */
170
    static public function extractUsernamesUsurpation(string $identifiant): array
171
172
173
174
175
176
177
178
179
    {
        if (strpos($identifiant, self::USURPATION_USERNAMES_SEP) > 0) {
            list($identifiant, $usernameUsurpe) = explode(self::USURPATION_USERNAMES_SEP, $identifiant, 2);

            return [
                $identifiant,
                $usernameUsurpe
            ];
        }
180

181
        return [];
Bertrand Gauthier's avatar
Bertrand Gauthier committed
182
183
    }

184
185
    /**
     * Authentifie l'identifiant et le mot de passe spécifiés.
186
     *
187
     * @param string $username Identifiant de connexion
188
189
     * @param string $credential Mot de passe
     * @return boolean
Bertrand Gauthier's avatar
Bertrand Gauthier committed
190
191
     * @throws \Laminas\Authentication\Adapter\Exception\ExceptionInterface
     * @throws \Laminas\Ldap\Exception\LdapException
192
     */
193
    public function authenticateUsername(string $username, string $credential): bool
194
195
196
197
198
    {
        // si 2 logins sont fournis, cela active l'usurpation d'identité (à n'utiliser que pour les tests) :
        // - le format attendu est "loginUsurpateur=loginUsurpé"
        // - le mot de passe attendu est celui du compte usurpateur (loginUsurpateur)
        $this->usernameUsurpe = null;
199
200
201
        $usernames = self::extractUsernamesUsurpation($username);
        if (count($usernames) === 2) {
            list ($username, $this->usernameUsurpe) = $usernames;
202
            if (!in_array($username, $this->moduleOptions->getUsurpationAllowedUsernames())) {
203
204
205
206
207
                $this->usernameUsurpe = null;
            }
        }

        // LDAP auth
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
        $result = $this->getLdapAuthAdapter()->setUsername($username)->setPassword($credential)->authenticate();

        // Envoi des erreurs LDAP dans un événement
        if (!$result->isValid()) {
            $messages = "LDAP ERROR : ";
            $errorMessages = $result->getMessages();
            if (count($errorMessages) > 0) {
                // On ne prend que les 2 premières lignes d'erreur (les suivantes contiennent souvent
                // les mots de passe de l'utilisateur, et les mot de passe dans les logs... bof bof).
                for ($i = 0; $i < 2 && count($errorMessages) >= $i; $i++) {
                    $messages .= $errorMessages[$i] . " ";
                }
            }
            $errorEvent = new Event(self::LDAP_AUTHENTIFICATION_FAIL, null, ['messages' => $messages]);
            $this->getEventManager()->triggerEvent($errorEvent);
        }
224

225
        $success = $result->isValid();
226

227
228
        // verif existence du login usurpé
        if ($this->usernameUsurpe) {
229
            // s'il nexiste pas, échec de l'authentification
230
231
232
            if (!@$this->getLdapAuthAdapter()->getLdap()->searchEntries(
                "(" . $this->moduleOptions->getLdapUsername() . "=$this->usernameUsurpe)"
            )) {
233
                $this->usernameUsurpe = null;
234
                $success = false;
235
236
            }
        }
237

238
239
        return $success;
    }
240

241
242
243
244
245
    /**
     * get ldap people mapper
     *
     * @return LdapPeopleMapper
     */
246
    public function getLdapPeopleMapper(): LdapPeopleMapper
247
248
249
250
251
252
253
254
255
256
    {
        return $this->ldapPeopleMapper;
    }

    /**
     * set ldap people mapper
     *
     * @param LdapPeopleMapper $mapper
     * @return self
     */
257
    public function setLdapPeopleMapper(LdapPeopleMapper $mapper): self
258
259
260
261
262
    {
        $this->ldapPeopleMapper = $mapper;
        return $this;
    }

Bertrand Gauthier's avatar
Bertrand Gauthier committed
263
    /**
264
     * @return ModuleOptions
Bertrand Gauthier's avatar
Bertrand Gauthier committed
265
     */
266
    public function getAppModuleOptions(): ModuleOptions
Bertrand Gauthier's avatar
Bertrand Gauthier committed
267
    {
268
        return $this->appModuleOptions;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
269
270
271
272
    }

    /**
     * get ldap connection adapter
273
     *
Bertrand Gauthier's avatar
Bertrand Gauthier committed
274
     * @return LdapAuthAdapter
Bertrand Gauthier's avatar
Bertrand Gauthier committed
275
     */
276
    public function getLdapAuthAdapter(): LdapAuthAdapter
Bertrand Gauthier's avatar
Bertrand Gauthier committed
277
    {
Bertrand Gauthier's avatar
Bertrand Gauthier committed
278
        if (null === $this->ldapAuthAdapter) {
279
            $options = [];
Bertrand Gauthier's avatar
Bertrand Gauthier committed
280
281
282
283
284
285
            if (($config = $this->getAppModuleOptions()->getLdap())) {
                foreach ($config['connection'] as $name => $connection) {
                    $options[$name] = $connection['params'];
                }
            }
            $this->ldapAuthAdapter = new LdapAuthAdapter($options); // NB: array(array)
Bertrand Gauthier's avatar
Bertrand Gauthier committed
286
        }
Bertrand Gauthier's avatar
Bertrand Gauthier committed
287
        return $this->ldapAuthAdapter;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
288
289
290
291
    }

    /**
     * set ldap connection adapter
292
     *
Bertrand Gauthier's avatar
Bertrand Gauthier committed
293
     * @param LdapAuthAdapter $authAdapter
294
     * @return self
Bertrand Gauthier's avatar
Bertrand Gauthier committed
295
     */
296
    public function setLdapAuthAdapter(LdapAuthAdapter $authAdapter): self
Bertrand Gauthier's avatar
Bertrand Gauthier committed
297
    {
Bertrand Gauthier's avatar
Bertrand Gauthier committed
298
        $this->ldapAuthAdapter = $authAdapter;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
299
300
301
302
        return $this;
    }

    /**
Bertrand Gauthier's avatar
Bertrand Gauthier committed
303
304
305
     * Retrieve EventManager instance
     *
     * @return EventManagerInterface
Bertrand Gauthier's avatar
Bertrand Gauthier committed
306
     */
Bertrand Gauthier's avatar
Bertrand Gauthier committed
307
    public function getEventManager()
Bertrand Gauthier's avatar
Bertrand Gauthier committed
308
    {
Bertrand Gauthier's avatar
Bertrand Gauthier committed
309
        return $this->eventManager;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
310
    }
311

Bertrand Gauthier's avatar
Bertrand Gauthier committed
312
    /**
313
     * @inheritdoc
Bertrand Gauthier's avatar
Bertrand Gauthier committed
314
     */
315
    public function setEventManager(EventManagerInterface $eventManager): self
Bertrand Gauthier's avatar
Bertrand Gauthier committed
316
    {
317
318
319
320
321
322
        $eventManager->setIdentifiers(
            [
                __NAMESPACE__,
                __CLASS__,
            ]
        );
Bertrand Gauthier's avatar
Bertrand Gauthier committed
323
        $this->eventManager = $eventManager;
324

Bertrand Gauthier's avatar
Bertrand Gauthier committed
325
        return $this;
Bertrand Gauthier's avatar
Bertrand Gauthier committed
326
327
    }
}