GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

LoginModel::resetFailedLoginCounterOfUser()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/**
4
 * LoginModel
5
 *
6
 * The login part of the model: Handles the login / logout stuff
7
 */
8
class LoginModel
9
{
10
    /**
11
     * Login process (for DEFAULT user accounts).
12
     *
13
     * @param $user_name string The user's name
14
     * @param $user_password string The user's password
15
     * @param $set_remember_me_cookie mixed Marker for usage of remember-me cookie feature
16
     *
17
     * @return bool success state
18
     */
19
    public static function login($user_name, $user_password, $set_remember_me_cookie = null)
20
    {
21
        // we do negative-first checks here, for simplicity empty username and empty password in one line
22
        if (empty($user_name) OR empty($user_password)) {
23
            Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_FIELD_EMPTY'));
24
            return false;
25
        }
26
27
        // checks if user exists, if login is not blocked (due to failed logins) and if password fits the hash
28
        $result = self::validateAndGetUser($user_name, $user_password);
29
30
        // check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
31
        if (!$result) {
32
            //No Need to give feedback here since whole validateAndGetUser controls gives a feedback
33
            return false;
34
        }
35
36
        // stop the user's login if account has been soft deleted
37
        if ($result->user_deleted == 1) {
38
            Session::add('feedback_negative', Text::get('FEEDBACK_DELETED'));
39
            return false;
40
        }
41
42
        // stop the user from logging in if user has a suspension, display how long they have left in the feedback.
43
        if ($result->user_suspension_timestamp != null && $result->user_suspension_timestamp - time() > 0) {
44
            $suspensionTimer = Text::get('FEEDBACK_ACCOUNT_SUSPENDED') . round(abs($result->user_suspension_timestamp - time())/60/60, 2) . " hours left";
45
            Session::add('feedback_negative', $suspensionTimer);
46
            return false;
47
        }
48
49
        // reset the failed login counter for that user (if necessary)
50
        if ($result->user_last_failed_login > 0) {
51
            self::resetFailedLoginCounterOfUser($result->user_name);
52
        }
53
54
        // save timestamp of this login in the database line of that user
55
        self::saveTimestampOfLoginOfUser($result->user_name);
56
57
        // if user has checked the "remember me" checkbox, then write token into database and into cookie
58
        if ($set_remember_me_cookie) {
59
            self::setRememberMeInDatabaseAndCookie($result->user_id);
60
        }
61
62
        // successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
63
        self::setSuccessfulLoginIntoSession(
64
            $result->user_id, $result->user_name, $result->user_email, $result->user_account_type
65
        );
66
67
        // return true to make clear the login was successful
68
        // maybe do this in dependence of setSuccessfulLoginIntoSession ?
69
        return true;
70
    }
71
72
    /**
73
     * Validates the inputs of the users, checks if password is correct etc.
74
     * If successful, user is returned
75
     *
76
     * @param $user_name
77
     * @param $user_password
78
     *
79
     * @return bool|mixed
80
     */
81
    private static function validateAndGetUser($user_name, $user_password)
82
    {
83
        // brute force attack mitigation: use session failed login count and last failed login for not found users.
84
        // block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
85
        // (limits user searches in database)
86
        if (Session::get('failed-login-count') >= 3 AND (Session::get('last-failed-login') > (time() - 30))) {
87
            Session::add('feedback_negative', Text::get('FEEDBACK_LOGIN_FAILED_3_TIMES'));
88
            return false;
89
        }
90
91
        // get all data of that user (to later check if password and password_hash fit)
92
        $result = UserModel::getUserDataByUsername($user_name);
93
94
        // check if that user exists. We don't give back a cause in the feedback to avoid giving an attacker details.
95
        // brute force attack mitigation: reset failed login counter because of found user
96
        if (!$result) {
97
98
            // increment the user not found count, helps mitigate user enumeration
99
            self::incrementUserNotFoundCounter();
100
101
            // user does not exist, but we won't to give a potential attacker this details, so we just use a basic feedback message
102
            Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
103
            return false;
104
        }
105
106
        // block login attempt if somebody has already failed 3 times and the last login attempt is less than 30sec ago
107
        if (($result->user_failed_logins >= 3) AND ($result->user_last_failed_login > (time() - 30))) {
108
            Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_WRONG_3_TIMES'));
109
            return false;
110
        }
111
112
        // if hash of provided password does NOT match the hash in the database: +1 failed-login counter
113
        if (!password_verify($user_password, $result->user_password_hash)) {
114
            self::incrementFailedLoginCounterOfUser($result->user_name);
115
            Session::add('feedback_negative', Text::get('FEEDBACK_USERNAME_OR_PASSWORD_WRONG'));
116
            return false;
117
        }
118
119
        // if user is not active (= has not verified account by verification mail)
120
        if ($result->user_active != 1) {
121
            Session::add('feedback_negative', Text::get('FEEDBACK_ACCOUNT_NOT_ACTIVATED_YET'));
122
            return false;
123
        }
124
125
        // reset the user not found counter
126
        self::resetUserNotFoundCounter();
127
128
        return $result;
129
    }
130
131
    /**
132
     * Reset the failed-login-count to 0.
133
     * Reset the last-failed-login to an empty string.
134
     */
135
    private static function resetUserNotFoundCounter()
136
    {
137
        Session::set('failed-login-count', 0);
138
        Session::set('last-failed-login', '');
139
    }
140
141
    /**
142
     * Increment the failed-login-count by 1.
143
     * Add timestamp to last-failed-login.
144
     */
145
    private static function incrementUserNotFoundCounter()
146
    {
147
        // Username enumeration prevention: set session failed login count and last failed login for users not found
148
        Session::set('failed-login-count', Session::get('failed-login-count') + 1);
149
        Session::set('last-failed-login', time());
150
    }
151
152
    /**
153
     * performs the login via cookie (for DEFAULT user account, FACEBOOK-accounts are handled differently)
154
     * TODO add throttling here ?
155
     *
156
     * @param $cookie string The cookie "remember_me"
157
     *
158
     * @return bool success state
159
     */
160
    public static function loginWithCookie($cookie)
161
    {
162
        // do we have a cookie ?
163
        if (!$cookie) {
164
            Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
165
            return false;
166
        }
167
168
        // before list(), check it can be split into 3 strings.
169
        if (count (explode(':', $cookie)) !== 3) {
170
            Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
171
            return false;
172
        }
173
174
        // check cookie's contents, check if cookie contents belong together or token is empty
175
        list ($user_id, $token, $hash) = explode(':', $cookie);
176
177
        // decrypt user id
178
        $user_id = Encryption::decrypt($user_id);
179
180
        if ($hash !== hash('sha256', $user_id . ':' . $token) OR empty($token) OR empty($user_id)) {
181
            Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
182
            return false;
183
        }
184
185
        // get data of user that has this id and this token
186
        $result = UserModel::getUserDataByUserIdAndToken($user_id, $token);
187
188
        // if user with that id and exactly that cookie token exists in database
189
        if ($result) {
190
191
            // successfully logged in, so we write all necessary data into the session and set "user_logged_in" to true
192
            self::setSuccessfulLoginIntoSession($result->user_id, $result->user_name, $result->user_email, $result->user_account_type);
193
194
            // save timestamp of this login in the database line of that user
195
            self::saveTimestampOfLoginOfUser($result->user_name);
196
197
            // NOTE: we don't set another remember_me-cookie here as the current cookie should always
198
            // be invalid after a certain amount of time, so the user has to login with username/password
199
            // again from time to time. This is good and safe ! ;)
200
201
            Session::add('feedback_positive', Text::get('FEEDBACK_COOKIE_LOGIN_SUCCESSFUL'));
202
            return true;
203
        } else {
204
            Session::add('feedback_negative', Text::get('FEEDBACK_COOKIE_INVALID'));
205
            return false;
206
        }
207
    }
208
209
    /**
210
     * Log out process: delete cookie, delete session
211
     */
212
    public static function logout()
213
    {
214
        $user_id = Session::get('user_id');
215
216
        self::deleteCookie($user_id);
217
218
        Session::destroy();
219
        Session::updateSessionId($user_id);
220
    }
221
222
    /**
223
     * The real login process: The user's data is written into the session.
224
     * Cheesy name, maybe rename. Also maybe refactoring this, using an array.
225
     *
226
     * @param $user_id
227
     * @param $user_name
228
     * @param $user_email
229
     * @param $user_account_type
230
     */
231
    public static function setSuccessfulLoginIntoSession($user_id, $user_name, $user_email, $user_account_type)
232
    {
233
        Session::init();
234
235
        // remove old and regenerate session ID.
236
        // It's important to regenerate session on sensitive actions,
237
        // and to avoid fixated session.
238
        // e.g. when a user logs in
239
        session_regenerate_id(true);
240
        $_SESSION = array();
241
242
        Session::set('user_id', $user_id);
243
        Session::set('user_name', $user_name);
244
        Session::set('user_email', $user_email);
245
        Session::set('user_account_type', $user_account_type);
246
        Session::set('user_provider_type', 'DEFAULT');
247
248
        // get and set avatars
249
        Session::set('user_avatar_file', AvatarModel::getPublicUserAvatarFilePathByUserId($user_id));
250
        Session::set('user_gravatar_image_url', AvatarModel::getGravatarLinkByEmail($user_email));
251
252
        // finally, set user as logged-in
253
        Session::set('user_logged_in', true);
254
255
        // update session id in database
256
        Session::updateSessionId($user_id, session_id());
257
258
        // set session cookie setting manually,
259
        // Why? because you need to explicitly set session expiry, path, domain, secure, and HTTP.
260
        // @see https://www.owasp.org/index.php/PHP_Security_Cheat_Sheet#Cookies
261
        setcookie(session_name(), session_id(), time() + Config::get('SESSION_RUNTIME'), Config::get('COOKIE_PATH'),
262
            Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
263
264
    }
265
266
    /**
267
     * Increments the failed-login counter of a user
268
     *
269
     * @param $user_name
270
     */
271
    public static function incrementFailedLoginCounterOfUser($user_name)
272
    {
273
        $database = DatabaseFactory::getFactory()->getConnection();
274
275
        $sql = "UPDATE users
276
                   SET user_failed_logins = user_failed_logins+1, user_last_failed_login = :user_last_failed_login
277
                 WHERE user_name = :user_name OR user_email = :user_name
278
                 LIMIT 1";
279
        $sth = $database->prepare($sql);
280
        $sth->execute(array(':user_name' => $user_name, ':user_last_failed_login' => time() ));
281
    }
282
283
    /**
284
     * Resets the failed-login counter of a user back to 0
285
     *
286
     * @param $user_name
287
     */
288
    public static function resetFailedLoginCounterOfUser($user_name)
289
    {
290
        $database = DatabaseFactory::getFactory()->getConnection();
291
292
        $sql = "UPDATE users
293
                   SET user_failed_logins = 0, user_last_failed_login = NULL
294
                 WHERE user_name = :user_name AND user_failed_logins != 0
295
                 LIMIT 1";
296
        $sth = $database->prepare($sql);
297
        $sth->execute(array(':user_name' => $user_name));
298
    }
299
300
    /**
301
     * Write timestamp of this login into database (we only write a "real" login via login form into the database,
302
     * not the session-login on every page request
303
     *
304
     * @param $user_name
305
     */
306
    public static function saveTimestampOfLoginOfUser($user_name)
307
    {
308
        $database = DatabaseFactory::getFactory()->getConnection();
309
310
        $sql = "UPDATE users SET user_last_login_timestamp = :user_last_login_timestamp
311
                WHERE user_name = :user_name LIMIT 1";
312
        $sth = $database->prepare($sql);
313
        $sth->execute(array(':user_name' => $user_name, ':user_last_login_timestamp' => time()));
314
    }
315
316
    /**
317
     * Write remember-me token into database and into cookie
318
     * Maybe splitting this into database and cookie part ?
319
     *
320
     * @param $user_id
321
     */
322
    public static function setRememberMeInDatabaseAndCookie($user_id)
323
    {
324
        $database = DatabaseFactory::getFactory()->getConnection();
325
326
        // generate 64 char random string
327
        $random_token_string = hash('sha256', mt_rand());
328
329
        // write that token into database
330
        $sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
331
        $sth = $database->prepare($sql);
332
        $sth->execute(array(':user_remember_me_token' => $random_token_string, ':user_id' => $user_id));
333
334
        // generate cookie string that consists of user id, random string and combined hash of both
335
        // never expose the original user id, instead, encrypt it.
336
        $cookie_string_first_part = Encryption::encrypt($user_id) . ':' . $random_token_string;
337
        $cookie_string_hash       = hash('sha256', $user_id . ':' . $random_token_string);
338
        $cookie_string            = $cookie_string_first_part . ':' . $cookie_string_hash;
339
340
        // set cookie, and make it available only for the domain created on (to avoid XSS attacks, where the
341
        // attacker could steal your remember-me cookie string and would login itself).
342
        // If you are using HTTPS, then you should set the "secure" flag (the second one from right) to true, too.
343
        // @see http://www.php.net/manual/en/function.setcookie.php
344
        setcookie('remember_me', $cookie_string, time() + Config::get('COOKIE_RUNTIME'), Config::get('COOKIE_PATH'),
345
            Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
346
    }
347
348
    /**
349
     * Deletes the cookie
350
     * It's necessary to split deleteCookie() and logout() as cookies are deleted without logging out too!
351
     * Sets the remember-me-cookie to ten years ago (3600sec * 24 hours * 365 days * 10).
352
     * that's obviously the best practice to kill a cookie @see http://stackoverflow.com/a/686166/1114320
353
     *
354
     * @param string $user_id
355
     */
356
    public static function deleteCookie($user_id = null)
357
    {
358
        // is $user_id was set, then clear remember_me token in database
359
        if (isset($user_id)) {
360
361
            $database = DatabaseFactory::getFactory()->getConnection();
362
363
            $sql = "UPDATE users SET user_remember_me_token = :user_remember_me_token WHERE user_id = :user_id LIMIT 1";
364
            $sth = $database->prepare($sql);
365
            $sth->execute(array(':user_remember_me_token' => null, ':user_id' => $user_id));
366
        }
367
368
        // delete remember_me cookie in browser
369
        setcookie('remember_me', false, time() - (3600 * 24 * 3650), Config::get('COOKIE_PATH'),
370
            Config::get('COOKIE_DOMAIN'), Config::get('COOKIE_SECURE'), Config::get('COOKIE_HTTP'));
371
    }
372
373
    /**
374
     * Returns the current state of the user's login
375
     *
376
     * @return bool user's login status
377
     */
378
    public static function isUserLoggedIn()
379
    {
380
        return Session::userIsLoggedIn();
381
    }
382
}
383