userAuthentication.php 11.2 KB
Newer Older
Prosper De Laure's avatar
Prosper De Laure committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
/*
 * Copyright (C) 2015 Maarch
 *
 * This file is part of bundle user.
 *
 * Bundle user is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Bundle user is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with bundle user.  If not, see <http://www.gnu.org/licenses/>.
 */
namespace bundle\auth\Controller;

/**
 * user authentication controller
 *
 * @package Auth
 * @author  Cyril VAZQUEZ <cyril.vazquez@maarch.org>
 */
class userAuthentication
{
    /**
     * The password encryption
     *
     * @var string
     **/
    protected $passwordEncryption;

    /**
     * The security policy of the password
     *
     * @var string
     **/
    protected $securityPolicy;

    /**
     * Constructor
     * @param object $sdoFactory         The user model
     * @param string $passwordEncryption The password encryption
     * @param array  $securityPolicy     The security policy
     */
    public function __construct(\dependency\sdo\Factory $sdoFactory, $passwordEncryption, $securityPolicy)
    {
        $this->sdoFactory = $sdoFactory;
        $this->passwordEncryption = $passwordEncryption;
        $this->securityPolicy = $securityPolicy;
    }

    /**
58
     * Authenticate a user
Prosper De Laure's avatar
Prosper De Laure committed
59
60
61
     * @param string $userName The user name
     * @param string $password The user password
     *
62
63
     * @throws \bundle\auth\Exception\authenticationException
     *
64
     * @return auth/account The user account object
Prosper De Laure's avatar
Prosper De Laure committed
65
66
67
     */
    public function login($userName, $password)
    {
Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
68
69
        // Check userAccount exists and get it
        $userAccount = $this->getUserByName($userName);
Prosper De Laure's avatar
Prosper De Laure committed
70

71
72
        $this->checkEnabled($userAccount);

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
73
74
        // Check password ans status
        $userLogin = $this->checkCredentials($userAccount, $password);
Prosper De Laure's avatar
Prosper De Laure committed
75

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
        if (isset($this->securityPolicy['sessionTimeout'])) {
            $tokenDuration = $this->securityPolicy['sessionTimeout'];
        } else {
            $tokenDuration = 86400;
        }
        
        // Set token
        $this->setToken($userLogin, $tokenDuration);
        
        // Require password change
        if ($this->securityPolicy['passwordValidity'] && $this->securityPolicy["passwordValidity"] != 0) {
            $diff = ($userLogin->lastLogin->getTimestamp() - $userAccount->passwordLastChange->getTimestamp()) / $tokenDuration;
            if ($diff > $this->securityPolicy['passwordValidity']) {
                throw \laabs::newException('auth/userPasswordChangeRequestException');
            }
Prosper De Laure's avatar
Prosper De Laure committed
91
92
        }

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
93
        if ($userAccount->passwordChangeRequired == true) {
94
            \laabs::setToken('TEMP-AUTH', $userLogin, $tokenDuration);
Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
            \laabs::unsetToken('AUTH');
            throw \laabs::newException('auth/userPasswordChangeRequestException');
        }

        return $userAccount;
    }

    /**
     * Log a remote user
     * @param string $userName
     *
     * @return auth/userAccount
     */
    public function logRemoteUser($userName)
    {
        // Check userAccount exists and get it
        $userAccount = $this->getUserByName($userName);
Prosper De Laure's avatar
Prosper De Laure committed
112

113
114
        $this->checkEnabled($userAccount);

Prosper De Laure's avatar
Prosper De Laure committed
115
116
117
118
        $userLogin = \laabs::newInstance('auth/userLogin');
        $userLogin->accountId = $userAccount->accountId;
        $userLogin->lastIp = $_SERVER["REMOTE_ADDR"];

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
        if (isset($this->securityPolicy['sessionTimeout'])) {
            $tokenDuration = $this->securityPolicy['sessionTimeout'];
        } else {
            $tokenDuration = 86400;
        }
        
        // Set token
        $this->setToken($userLogin, $tokenDuration);

        return $userAccount;
    }

    /**
     * Get user from userName
     * @param string $userName
     * 
     * @return auth/account
     * @throws auth/authenticationException when username not found
     */
    public function getUserByName(string $userName)
    {
        $exists = $this->sdoFactory->exists('auth/account', array('accountName' => $userName));

        if (!$exists) {
            throw \laabs::newException('auth/authenticationException', 'Username and / or password invalid', 401);
        }

        return $this->sdoFactory->read('auth/account', array('accountName' => $userName));
    }

    /**
     * Check user password and hability to login
     * @param object $userAccount
     * 
     * @return object
     */
    protected function checkCredentials($userAccount, $password)
    {
        $currentDate = \laabs::newTimestamp();

        // Create user login object
        $userLogin = \laabs::newInstance('auth/userLogin');
        $userLogin->accountId = $userAccount->accountId;
        $userLogin->lastIp = $_SERVER["REMOTE_ADDR"];

Prosper De Laure's avatar
Prosper De Laure committed
164
        // Check password
Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
        if (!password_verify($password, $userAccount->password) && hash($this->passwordEncryption, $password) != $userAccount->password) {
            // Update bad password count
            $userLogin->badPasswordCount = $userAccount->badPasswordCount + 1;
            $this->sdoFactory->update($userLogin, 'auth/account');

            // If count exceeds max attempts, lock user
            if ($this->securityPolicy['loginAttempts']
                && $userLogin->badPasswordCount > $this->securityPolicy['loginAttempts'] - 1
            ) {
                $userAccountController = \laabs::newController('auth/userAccount');
                $userAccountController->lock($userLogin->accountId, true);

                $eventController = \laabs::newController('audit/event');
                $eventController->add(
                    "auth/userAccount/updateLock_userAccountId_",
                    array("accountId" => $userLogin->accountId),
                    null,
                    true,
                    true
                );
Prosper De Laure's avatar
Prosper De Laure committed
185
186
            }

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
187
188
189
190
191
            throw \laabs::newException('auth/authenticationException', 'Username and / or password invalid', 401);
        }

        if (password_needs_rehash($userAccount->password, PASSWORD_DEFAULT)) {
            $userLogin->password = password_hash($password, PASSWORD_DEFAULT);
192
        }
193
        
194
195
196
197
198
199
200
201
202
        // Check locked
        if ($userAccount->locked == true) {
            if (!isset($this->securityPolicy['lockDelay']) // No delay while locked
                || $this->securityPolicy['lockDelay'] == 0 // Unlimited delay
                || !isset($userAccount->lockDate)          // Delay but no date for lock so unlimited
                || ($currentDate->getTimestamp() - $userAccount->lockDate->getTimestamp()) < ($this->securityPolicy['lockDelay']) // Date + delay upper than current date
            ) {
                throw \laabs::newException('auth/authenticationException', 'User %1$s is locked', 403, null, array($userName));
            }
203
204
        }

Prosper De Laure's avatar
Prosper De Laure committed
205
206
207
208
        // Login success, update user account values
        $userLogin->badPasswordCount = 0;
        $userLogin->locked = false;
        $userLogin->lockDate = null;
209
        $userLogin->tokenDate = null;
Prosper De Laure's avatar
Prosper De Laure committed
210
211
212
213
        $userLogin->lastLogin = $currentDate;

        $this->sdoFactory->update($userLogin, 'auth/account');

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
214
215
        return $userLogin;
    }
Prosper De Laure's avatar
Prosper De Laure committed
216

Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
217
218
219
220
221
222
223
    /**
     * Sets the auth token
     * @param object $userLogin
     * @param int    $tokenDuration
     */
    public function setToken($userLogin, $tokenDuration)
    {
Prosper De Laure's avatar
Prosper De Laure committed
224
        $accountToken = new \StdClass();
Cyril Vazquez's avatar
Rework    
Cyril Vazquez committed
225
        $accountToken->accountId = $userLogin->accountId;
226
        $userToken = \laabs::setToken('AUTH', $accountToken, $tokenDuration);
Prosper De Laure's avatar
Prosper De Laure committed
227
228
229
230
231
232
233
234
235
    }

    /**
     * Get form to edit user information
     * @param string $userName    The user's name
     * @param string $oldPassword The user's old password
     * @param string $newPassword The user's new password
     * @param string $requestPath The requested path
     *
236
     * @return mixed The requested path if the account exists, false if it doesn't
Prosper De Laure's avatar
Prosper De Laure committed
237
238
239
     */
    public function definePassword($userName, $oldPassword, $newPassword, $requestPath)
    {
240
        $tempToken = \laabs::getToken('TEMP-AUTH');
241
242
243
244
        if (!$this->sdoFactory->exists('auth/account', array('accountName' => $userName))) {
            return false;
        }
        $userAccount = $this->sdoFactory->read('auth/account', array('accountName' => $userName));
245

246
        if (!is_null($tempToken) && $tempToken->accountId == $userAccount->accountId) {
247
            $this->checkPasswordPolicies($newPassword);
Prosper De Laure's avatar
Prosper De Laure committed
248

249
            if (password_verify($newPassword, $userAccount->password)) {
250
                throw new \core\Exception\ForbiddenException("The password is the same as the precedent.", 403);
Prosper De Laure's avatar
Prosper De Laure committed
251
252
            }

253
            $userAccount->password = password_hash($newPassword, PASSWORD_DEFAULT);
Prosper De Laure's avatar
Prosper De Laure committed
254
255
256
257
258
259
260
261
262
263
264
265
266
            $userAccount->passwordLastChange = \laabs::newTimestamp();
            $userAccount->passwordChangeRequired = false;
            $this->sdoFactory->update($userAccount, 'auth/account');

            $this->login($userAccount->accountName, $newPassword, $requestPath);
            \laabs::unsetToken('TEMP-AUTH');

            return $requestPath;
        }

        return false;
    }

267
268
269
270
    /**
     * Validate the new password
     * @param string $newPassword The user's new password
     */
271
272
    public function checkPasswordPolicies($newPassword)
    {
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
        if ($this->securityPolicy['passwordMinLength'] && strlen($newPassword) < $this->securityPolicy['passwordMinLength']) {
            throw new \core\Exception\ForbiddenException("The password is too short.", 403);
        }

        if ($this->securityPolicy['passwordRequiresSpecialChars'] && ctype_alnum($newPassword)) {
            throw new \core\Exception\ForbiddenException("The password must contain special characters.", 403);
        }

        if ($this->securityPolicy['passwordRequiresDigits'] && !preg_match('~[0-9]~', $newPassword)) {
            throw new \core\Exception\ForbiddenException("The password must contain digits.", 403);
        }

        if ($this->securityPolicy['passwordRequiresMixedCase'] && (!preg_match('~[A-Z]~', $newPassword) || !preg_match('~[a-z]~', $newPassword))) {
            throw new \core\Exception\ForbiddenException("The password must contain upper and lower case.", 403);
        }
    }

290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
    /**
     * Check if user is enabled
     * @param object $userAccount
     * 
     * @return object
     */
    protected function checkEnabled($userAccount)
    {
        if ($userAccount->enabled != true) {
            $e = \laabs::newException(
                'auth/authenticationException',
                'User %1$s is disabled',
                403,
                null,
                array($userName)
            );
            throw $e;
        }
    }

Prosper De Laure's avatar
Prosper De Laure committed
310
311
312
313
314
    /**
     * Log out a user
     */
    public function logout()
    {
315
316
        $userAccount = $this->sdoFactory->read('auth/account', \laabs::getToken('AUTH'));
        $authentication = json_decode($userAccount->authentication);
317
        $authentication->sessionId = null;
318
319
320
321
        $userAccount->authentication = json_encode($authentication);

        $this->sdoFactory->update($userAccount, "auth/account");

322
323
        \laabs::unsetToken("AUTH");
        \laabs::unsetToken("ORGANIZATION");
Prosper De Laure's avatar
Prosper De Laure committed
324
325
    }
}