Failed Conditions
Push — newinternal ( b66232...216d62 )
by Simon
16:33 queued 06:35
created

Pages/UserAuth/Login/LoginCredentialPageBase.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Pages\UserAuth\Login;
10
11
use PDO;
12
use Waca\DataObjects\User;
13
use Waca\Exceptions\ApplicationLogicException;
14
use Waca\Exceptions\OAuthException;
15
use Waca\Helpers\OAuthUserHelper;
16
use Waca\PdoDatabase;
17
use Waca\Security\AuthenticationManager;
18
use Waca\SessionAlert;
19
use Waca\Tasks\InternalPageBase;
20
use Waca\WebRequest;
21
22
abstract class LoginCredentialPageBase extends InternalPageBase
23
{
24
    /** @var User */
25
    protected $partialUser = null;
26
    protected $nextPageMap = array(
27
        'yubikeyotp' => 'otp',
28
        'totp'       => 'otp',
29
        'scratch'    => 'otp',
30
        'u2f'        => 'u2f',
31
    );
32
    protected $names = array(
33
        'yubikeyotp' => 'Yubikey OTP',
34
        'totp'       => 'TOTP (phone code generator)',
35
        'scratch'    => 'scratch token',
36
        'u2f'        => 'U2F security token',
37
    );
38
39
    /**
40
     * Main function for this page, when no specific actions are called.
41
     * @return void
42
     */
43
    protected function main()
44
    {
45
        if (!$this->enforceHttps()) {
46
            return;
47
        }
48
49
        if (WebRequest::wasPosted()) {
50
            $this->validateCSRFToken();
51
52
            $database = $this->getDatabase();
53
            try {
54
                list($partialId, $partialStage) = WebRequest::getAuthPartialLogin();
55
56
                if ($partialStage === null) {
57
                    $partialStage = 1;
58
                }
59
60
                if ($partialId === null) {
61
                    $username = WebRequest::postString('username');
62
63
                    if ($username === null || trim($username) === '') {
64
                        throw new ApplicationLogicException('No username specified.');
65
                    }
66
67
                    $user = User::getByUsername($username, $database);
68
                }
69
                else {
70
                    $user = User::getById($partialId, $database);
71
                }
72
73
                if ($user === false) {
74
                    throw new ApplicationLogicException("Authentication failed");
75
                }
76
77
                $authMan = new AuthenticationManager($database, $this->getSiteConfiguration(),
78
                    $this->getHttpHelper());
79
80
                $credential = $this->getProviderCredentials();
81
82
                $authResult = $authMan->authenticate($user, $credential, $partialStage);
83
84
                if ($authResult === AuthenticationManager::AUTH_FAIL) {
85
                    throw new ApplicationLogicException("Authentication failed");
86
                }
87
88
                if ($authResult === AuthenticationManager::AUTH_REQUIRE_NEXT_STAGE) {
89
                    $this->processJumpNextStage($user, $partialStage, $database);
90
91
                    return;
92
                }
93
94
                if ($authResult === AuthenticationManager::AUTH_OK) {
95
                    $this->processLoginSuccess($user);
96
97
                    return;
98
                }
99
            }
100
            catch (ApplicationLogicException $ex) {
101
                WebRequest::clearAuthPartialLogin();
102
103
                SessionAlert::error($ex->getMessage());
104
                $this->redirect('login');
105
106
                return;
107
            }
108
        }
109
        else {
110
            $this->assign('showSignIn', true);
111
112
            $this->setupPartial();
113
            $this->assignCSRFToken();
114
            $this->providerSpecificSetup();
115
        }
116
    }
117
118
    protected function isProtectedPage()
119
    {
120
        return false;
121
    }
122
123
    /**
124
     * Enforces HTTPS on the login form
125
     *
126
     * @return bool
127
     */
128
    private function enforceHttps()
129
    {
130
        if ($this->getSiteConfiguration()->getUseStrictTransportSecurity() !== false) {
131
            if (WebRequest::isHttps()) {
132
                // Client can clearly use HTTPS, so let's enforce it for all connections.
133
                $this->headerQueue[] = "Strict-Transport-Security: max-age=15768000";
134
            }
135
            else {
136
                // This is the login form, not the request form. We need protection here.
137
                $this->redirectUrl('https://' . WebRequest::serverName() . WebRequest::requestUri());
138
139
                return false;
140
            }
141
        }
142
143
        return true;
144
    }
145
146
    protected abstract function providerSpecificSetup();
0 ignored issues
show
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
147
148
    protected function setupPartial()
149
    {
150
        $database = $this->getDatabase();
151
152
        // default stuff
153
        $this->assign('alternatives', array()); // 'u2f' => array('U2F token'), 'otp' => array('TOTP', 'scratch', 'yubiotp')));
154
155
        // is this stage one?
156
        list($partialId, $partialStage) = WebRequest::getAuthPartialLogin();
157
        if ($partialStage === null || $partialId === null) {
158
            WebRequest::clearAuthPartialLogin();
159
        }
160
161
        // Check to see if we have a partial login in progress
162
        $username = null;
163
        if ($partialId !== null) {
164
            // Yes, enforce this username
165
            $this->partialUser = User::getById($partialId, $database);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Waca\DataObjects\User::...($partialId, $database) can also be of type false. However, the property $partialUser is declared as type object<Waca\DataObjects\User>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
166
            $username = $this->partialUser->getUsername();
167
168
            $this->setupAlternates($this->partialUser, $partialStage, $database);
0 ignored issues
show
It seems like $this->partialUser can also be of type false; however, Waca\Pages\UserAuth\Logi...Base::setupAlternates() does only seem to accept object<Waca\DataObjects\User>, did you maybe forget to handle an error condition?
Loading history...
169
        }
170
        else {
171
            // No, see if we've preloaded a username
172
            $preloadUsername = WebRequest::getString('tplUsername');
173
            if ($preloadUsername !== null) {
174
                $username = $preloadUsername;
175
            }
176
        }
177
178
        if ($partialStage === null) {
179
            $partialStage = 1;
180
        }
181
182
        $this->assign('partialStage', $partialStage);
183
        $this->assign('username', $username);
184
    }
185
186
    /**
187
     * Redirect the user back to wherever they came from after a successful login
188
     *
189
     * @param User $user
190
     */
191
    protected function goBackWhenceYouCame(User $user)
192
    {
193
        // Redirect to wherever the user came from
194
        $redirectDestination = WebRequest::clearPostLoginRedirect();
195
        if ($redirectDestination !== null) {
196
            $this->redirectUrl($redirectDestination);
197
        }
198
        else {
199
            if ($user->isNewUser()) {
200
                // home page isn't allowed, go to preferences instead
201
                $this->redirect('preferences');
202
            }
203
            else {
204
                // go to the home page
205
                $this->redirect('');
206
            }
207
        }
208
    }
209
210
    private function processLoginSuccess(User $user)
211
    {
212
        // Touch force logout
213
        $user->setForceLogout(false);
214
        $user->save();
215
216
        $oauth = new OAuthUserHelper($user, $this->getDatabase(), $this->getOAuthProtocolHelper(),
217
            $this->getSiteConfiguration());
218
219
        if ($oauth->isFullyLinked()) {
220
            try {
221
                // Reload the user's identity ticket.
222
                $oauth->refreshIdentity();
223
224
                // Check for blocks
225
                if ($oauth->getIdentity()->getBlocked()) {
226
                    // blocked!
227
                    SessionAlert::error("You are currently blocked on-wiki. You will not be able to log in until you are unblocked.");
228
                    $this->redirect('login');
229
230
                    return;
231
                }
232
            }
233
            catch (OAuthException $ex) {
234
                // Oops. Refreshing ticket failed. Force a re-auth.
235
                $authoriseUrl = $oauth->getRequestToken();
236
                WebRequest::setOAuthPartialLogin($user);
237
                $this->redirectUrl($authoriseUrl);
238
239
                return;
240
            }
241
        }
242
243
        if (($this->getSiteConfiguration()->getEnforceOAuth() && !$oauth->isFullyLinked())
244
            || $oauth->isPartiallyLinked()
245
        ) {
246
            $authoriseUrl = $oauth->getRequestToken();
247
            WebRequest::setOAuthPartialLogin($user);
248
            $this->redirectUrl($authoriseUrl);
249
250
            return;
251
        }
252
253
        WebRequest::setLoggedInUser($user);
254
255
        $this->goBackWhenceYouCame($user);
256
    }
257
258
    protected abstract function getProviderCredentials();
0 ignored issues
show
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
259
260
    /**
261
     * @param User        $user
262
     * @param int         $partialStage
263
     * @param PdoDatabase $database
264
     *
265
     * @throws ApplicationLogicException
266
     */
267
    private function processJumpNextStage(User $user, $partialStage, PdoDatabase $database)
268
    {
269
        WebRequest::setAuthPartialLogin($user->getId(), $partialStage + 1);
270
271
        $sql = 'SELECT type FROM credential WHERE user = :user AND factor = :stage AND disabled = 0 ORDER BY priority';
272
        $statement = $database->prepare($sql);
273
        $statement->execute(array(':user' => $user->getId(), ':stage' => $partialStage + 1));
274
        $nextStage = $statement->fetchColumn();
275
        $statement->closeCursor();
276
277
        if (!isset($this->nextPageMap[$nextStage])) {
278
            throw new ApplicationLogicException('Unknown page handler for next authentication stage.');
279
        }
280
281
        $this->redirect("login/" . $this->nextPageMap[$nextStage]);
282
    }
283
284
    private function setupAlternates(User $user, $partialStage, PdoDatabase $database)
285
    {
286
        // get the providers available
287
        $sql = 'SELECT type FROM credential WHERE user = :user AND factor = :stage AND disabled = 0';
288
        $statement = $database->prepare($sql);
289
        $statement->execute(array(':user' => $user->getId(), ':stage' => $partialStage));
290
        $alternates = $statement->fetchAll(PDO::FETCH_COLUMN);
291
292
        $types = array();
293
        foreach ($alternates as $item) {
294
            $type = $this->nextPageMap[$item];
295
            if (!isset($types[$type])) {
296
                $types[$type] = array();
297
            }
298
299
            $types[$type][] = $item;
300
        }
301
302
        $userOptions = array();
303
        if (get_called_class() === PageOtpLogin::class) {
304
            $userOptions = $this->setupUserOptionsForType($types, 'u2f', $userOptions);
305
        }
306
307
        if (get_called_class() === PageU2FLogin::class) {
308
            $userOptions = $this->setupUserOptionsForType($types, 'otp', $userOptions);
309
        }
310
311
        $this->assign('alternatives', $userOptions);
312
    }
313
314
    /**
315
     * @param $types
316
     * @param $type
317
     * @param $userOptions
318
     *
319
     * @return mixed
320
     */
321
    private function setupUserOptionsForType($types, $type, $userOptions)
322
    {
323
        if (isset($types[$type])) {
324
            $options = $types[$type];
325
326
            array_walk($options, function(&$val) {
327
                $val = $this->names[$val];
328
            });
329
330
            $userOptions[$type] = $options;
331
        }
332
333
        return $userOptions;
334
    }
335
}
336