Issues (3069)

unit/Plugins/Auth/AuthenticationCookieTest.php (6 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\Tests\Plugins\Auth;
6
7
use Fig\Http\Message\StatusCodeInterface;
8
use PhpMyAdmin\Config;
9
use PhpMyAdmin\Current;
10
use PhpMyAdmin\Dbal\DatabaseInterface;
11
use PhpMyAdmin\Exceptions\AuthenticationFailure;
12
use PhpMyAdmin\Exceptions\ExitException;
13
use PhpMyAdmin\Plugins\Auth\AuthenticationCookie;
14
use PhpMyAdmin\ResponseRenderer;
15
use PhpMyAdmin\Tests\AbstractTestCase;
16
use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseRendererStub;
17
use PHPUnit\Framework\Attributes\CoversClass;
18
use PHPUnit\Framework\Attributes\DataProvider;
19
use PHPUnit\Framework\Attributes\Medium;
20
use ReflectionException;
21
use ReflectionMethod;
22
use ReflectionProperty;
23
use Throwable;
24
25
use function base64_decode;
26
use function base64_encode;
27
use function is_readable;
28
use function json_decode;
29
use function json_encode;
30
use function mb_strlen;
31
use function ob_get_clean;
32
use function ob_start;
33
use function random_bytes;
34
use function str_repeat;
35
use function str_shuffle;
36
use function time;
37
38
use const SODIUM_CRYPTO_SECRETBOX_KEYBYTES;
39
40
#[CoversClass(AuthenticationCookie::class)]
41
#[Medium]
42
class AuthenticationCookieTest extends AbstractTestCase
43
{
44
    protected AuthenticationCookie $object;
45
46
    /**
47
     * Configures global environment.
48
     */
49
    protected function setUp(): void
50
    {
51
        parent::setUp();
52
53
        $this->setLanguage();
54
55
        $this->setGlobalConfig();
56
57
        DatabaseInterface::$instance = $this->createDatabaseInterface();
58
        Current::$database = 'db';
59
        Current::$table = 'table';
60
        $_POST['pma_password'] = '';
61
        $this->object = new AuthenticationCookie();
62
        $_SERVER['PHP_SELF'] = '/phpmyadmin/index.php';
63
        Config::getInstance()->selectedServer['DisableIS'] = false;
64
        AuthenticationCookie::$connectionError = '';
65
    }
66
67
    /**
68
     * tearDown for test cases
69
     */
70
    protected function tearDown(): void
71
    {
72
        parent::tearDown();
73
74
        unset($this->object);
75
    }
76
77
    public function testAuthErrorAJAX(): void
78
    {
79
        AuthenticationCookie::$connectionError = 'Error';
80
81
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
82
        $responseRenderer = ResponseRenderer::getInstance();
83
        $responseRenderer->setAjax(true);
84
85
        $response = $this->object->showLoginForm();
86
87
        self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode());
88
        $body = (string) $response->getBody();
89
        self::assertJson($body);
90
        $json = json_decode($body, true);
91
        self::assertIsArray($json);
92
        self::assertArrayHasKey('success', $json);
93
        self::assertFalse($json['success']);
94
        self::assertArrayHasKey('redirect_flag', $json);
95
        self::assertSame('1', $json['redirect_flag']);
96
    }
97
98
    public function testAuthError(): void
99
    {
100
        $_REQUEST = [];
101
102
        $_REQUEST['old_usr'] = '';
103
        $config = Config::getInstance();
104
        $config->settings['LoginCookieRecall'] = true;
105
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
106
        $this->object->user = 'pmauser';
107
        AuthenticationCookie::$authServer = 'localhost';
108
109
        AuthenticationCookie::$connectionError = 'Error';
110
        $config->set('Lang', 'en');
111
        $config->settings['AllowArbitraryServer'] = true;
112
        $config->settings['CaptchaApi'] = '';
113
        $config->settings['CaptchaRequestParam'] = '';
114
        $config->settings['CaptchaResponseParam'] = '';
115
        $config->settings['CaptchaLoginPrivateKey'] = '';
116
        $config->settings['CaptchaLoginPublicKey'] = '';
117
        Current::$database = 'testDb';
118
        Current::$table = 'testTable';
119
        $config->settings['Servers'] = [1, 2];
120
121
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null);
122
123
        $response = $this->object->showLoginForm();
124
125
        $result = (string) $response->getBody();
126
127
        self::assertStringContainsString(' id="imLogo"', $result);
128
129
        self::assertStringContainsString('<div class="alert alert-danger" role="alert">', $result);
130
131
        self::assertStringContainsString(
132
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form" ' .
133
            'class="disableAjax hide js-show">',
134
            $result,
135
        );
136
137
        self::assertStringContainsString(
138
            '<input type="text" name="pma_servername" id="serverNameInput" value="localhost"',
139
            $result,
140
        );
141
142
        self::assertStringContainsString(
143
            '<input type="text" name="pma_username" id="input_username" ' .
144
            'value="pmauser" class="form-control" autocomplete="username" spellcheck="false" autofocus>',
145
            $result,
146
        );
147
148
        self::assertStringContainsString(
149
            '<input type="password" name="pma_password" id="input_password" ' .
150
            'value="" class="form-control" autocomplete="current-password" spellcheck="false">',
151
            $result,
152
        );
153
154
        self::assertStringContainsString(
155
            '<select name="server" id="select_server" class="form-select" ' .
156
            'onchange="document.forms[\'login_form\'].' .
157
            'elements[\'pma_servername\'].value = \'\'">',
158
            $result,
159
        );
160
161
        self::assertStringContainsString('<input type="hidden" name="db" value="testDb">', $result);
162
163
        self::assertStringContainsString('<input type="hidden" name="table" value="testTable">', $result);
164
    }
165
166
    public function testAuthCaptcha(): void
167
    {
168
        $_REQUEST['old_usr'] = '';
169
        $config = Config::getInstance();
170
        $config->settings['LoginCookieRecall'] = false;
171
172
        $config->set('Lang', '');
173
        $config->settings['AllowArbitraryServer'] = false;
174
        $config->settings['Servers'] = [1];
175
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
176
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
177
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
178
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
179
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
180
        Current::$server = 2;
181
182
        $responseStub = new ResponseRendererStub();
183
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
184
185
        $response = $this->object->showLoginForm();
186
187
        $result = (string) $response->getBody();
188
189
        self::assertStringContainsString('id="imLogo"', $result);
190
191
        // Check for language selection if locales are there
192
        $loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
193
        if (is_readable($loc)) {
194
            self::assertStringContainsString(
195
                '<select name="lang" class="form-select autosubmit" lang="en" dir="ltr"'
196
                . ' id="languageSelect" aria-labelledby="languageSelectLabel">',
197
                $result,
198
            );
199
        }
200
201
        self::assertStringContainsString(
202
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
203
            ' class="disableAjax hide js-show" autocomplete="off">',
204
            $result,
205
        );
206
207
        self::assertStringContainsString('<input type="hidden" name="server" value="2">', $result);
208
209
        self::assertStringContainsString(
210
            '<script src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>',
211
            $result,
212
        );
213
214
        self::assertStringContainsString(
215
            '<input class="btn btn-primary g-recaptcha" data-sitekey="testpubkey"'
216
            . ' data-callback="recaptchaCallback" value="Log in" type="submit" id="input_go">',
217
            $result,
218
        );
219
    }
220
221
    public function testAuthCaptchaCheckbox(): void
222
    {
223
        $_REQUEST['old_usr'] = '';
224
        $config = Config::getInstance();
225
        $config->settings['LoginCookieRecall'] = false;
226
227
        $config->set('Lang', '');
228
        $config->settings['AllowArbitraryServer'] = false;
229
        $config->settings['Servers'] = [1];
230
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
231
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
232
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
233
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
234
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
235
        $config->settings['CaptchaMethod'] = 'checkbox';
236
        Current::$server = 2;
237
238
        $responseStub = new ResponseRendererStub();
239
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
240
241
        $response = $this->object->showLoginForm();
242
243
        $result = (string) $response->getBody();
244
245
        self::assertStringContainsString('id="imLogo"', $result);
246
247
        // Check for language selection if locales are there
248
        $loc = LOCALE_PATH . '/cs/LC_MESSAGES/phpmyadmin.mo';
249
        if (is_readable($loc)) {
250
            self::assertStringContainsString(
251
                '<select name="lang" class="form-select autosubmit" lang="en" dir="ltr"'
252
                . ' id="languageSelect" aria-labelledby="languageSelectLabel">',
253
                $result,
254
            );
255
        }
256
257
        self::assertStringContainsString(
258
            '<form method="post" id="login_form" action="index.php?route=/" name="login_form"' .
259
            ' class="disableAjax hide js-show" autocomplete="off">',
260
            $result,
261
        );
262
263
        self::assertStringContainsString('<input type="hidden" name="server" value="2">', $result);
264
265
        self::assertStringContainsString(
266
            '<script src="https://www.google.com/recaptcha/api.js?hl=en" async defer></script>',
267
            $result,
268
        );
269
270
        self::assertStringContainsString('<div class="g-recaptcha" data-sitekey="testpubkey"></div>', $result);
271
272
        self::assertStringContainsString(
273
            '<input class="btn btn-primary" value="Log in" type="submit" id="input_go">',
274
            $result,
275
        );
276
    }
277
278
    public function testAuthHeader(): void
279
    {
280
        $config = Config::getInstance();
281
        $config->settings['LoginCookieDeleteAll'] = false;
282
        $config->settings['Servers'] = [1];
283
284
        $responseStub = new ResponseRendererStub();
285
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
286
287
        $config->selectedServer['LogoutURL'] = 'https://example.com/logout';
288
        $config->selectedServer['auth_type'] = 'cookie';
289
290
        $this->object->logOut();
291
292
        $response = $responseStub->getResponse();
293
        self::assertSame(['https://example.com/logout'], $response->getHeader('Location'));
294
        self::assertSame(302, $response->getStatusCode());
295
    }
296
297
    public function testAuthHeaderPartial(): void
298
    {
299
        $config = Config::getInstance();
300
        $config->settings['LoginCookieDeleteAll'] = false;
301
        $config->settings['Servers'] = [1, 2, 3];
302
        $config->selectedServer['LogoutURL'] = 'https://example.com/logout';
303
        $config->selectedServer['auth_type'] = 'cookie';
304
305
        $_COOKIE['pmaAuth-2'] = '';
306
307
        $responseStub = new ResponseRendererStub();
308
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
309
310
        $this->object->logOut();
311
312
        $response = $responseStub->getResponse();
313
        self::assertSame(['/phpmyadmin/index.php?route=/&server=2&lang=en'], $response->getHeader('Location'));
314
        self::assertSame(302, $response->getStatusCode());
315
    }
316
317
    public function testAuthCheckCaptcha(): void
318
    {
319
        $config = Config::getInstance();
320
        $config->settings['CaptchaApi'] = 'https://www.google.com/recaptcha/api.js';
321
        $config->settings['CaptchaRequestParam'] = 'g-recaptcha';
322
        $config->settings['CaptchaResponseParam'] = 'g-recaptcha-response';
323
        $config->settings['CaptchaLoginPrivateKey'] = 'testprivkey';
324
        $config->settings['CaptchaLoginPublicKey'] = 'testpubkey';
325
        $_POST['g-recaptcha-response'] = '';
326
        $_POST['pma_username'] = 'testPMAUser';
327
328
        self::assertFalse(
329
            $this->object->readCredentials(),
330
        );
331
332
        self::assertSame(
333
            'Missing Captcha verification, maybe it has been blocked by adblock?',
334
            AuthenticationCookie::$connectionError,
335
        );
336
    }
337
338
    public function testLogoutDelete(): void
339
    {
340
        $responseStub = new ResponseRendererStub();
341
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
342
343
        $config = Config::getInstance();
344
        $config->settings['CaptchaApi'] = '';
345
        $config->settings['CaptchaRequestParam'] = '';
346
        $config->settings['CaptchaResponseParam'] = '';
347
        $config->settings['CaptchaLoginPrivateKey'] = '';
348
        $config->settings['CaptchaLoginPublicKey'] = '';
349
        $config->settings['LoginCookieDeleteAll'] = true;
350
        $config->set('PmaAbsoluteUri', '');
351
        $config->settings['Servers'] = [1];
352
353
        $_COOKIE['pmaAuth-0'] = 'test';
354
355
        $this->object->logOut();
356
357
        $response = $responseStub->getResponse();
358
        self::assertSame(['/phpmyadmin/index.php?route=/'], $response->getHeader('Location'));
359
        self::assertSame(302, $response->getStatusCode());
360
361
        self::assertArrayNotHasKey('pmaAuth-0', $_COOKIE);
362
    }
363
364
    public function testLogout(): void
365
    {
366
        $responseStub = new ResponseRendererStub();
367
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
368
369
        $config = Config::getInstance();
370
        $config->settings['CaptchaApi'] = '';
371
        $config->settings['CaptchaRequestParam'] = '';
372
        $config->settings['CaptchaResponseParam'] = '';
373
        $config->settings['CaptchaLoginPrivateKey'] = '';
374
        $config->settings['CaptchaLoginPublicKey'] = '';
375
        $config->settings['LoginCookieDeleteAll'] = false;
376
        $config->set('PmaAbsoluteUri', '');
377
        $config->settings['Servers'] = [1];
378
        $config->selectedServer = ['auth_type' => 'cookie'];
379
380
        $_COOKIE['pmaAuth-1'] = 'test';
381
382
        $this->object->logOut();
383
384
        $response = $responseStub->getResponse();
385
        self::assertSame(['/phpmyadmin/index.php?route=/'], $response->getHeader('Location'));
386
        self::assertSame(302, $response->getStatusCode());
387
388
        self::assertArrayNotHasKey('pmaAuth-1', $_COOKIE);
389
    }
390
391
    public function testAuthCheckArbitrary(): void
392
    {
393
        $config = Config::getInstance();
394
        $config->settings['CaptchaApi'] = '';
395
        $config->settings['CaptchaRequestParam'] = '';
396
        $config->settings['CaptchaResponseParam'] = '';
397
        $config->settings['CaptchaLoginPrivateKey'] = '';
398
        $config->settings['CaptchaLoginPublicKey'] = '';
399
        $_REQUEST['old_usr'] = '';
400
        $_POST['pma_username'] = 'testPMAUser';
401
        $_REQUEST['pma_servername'] = 'testPMAServer';
402
        $_POST['pma_password'] = 'testPMAPSWD';
403
        $config->settings['AllowArbitraryServer'] = true;
404
405
        self::assertTrue(
406
            $this->object->readCredentials(),
407
        );
408
409
        self::assertSame('testPMAUser', $this->object->user);
410
411
        self::assertSame('testPMAPSWD', $this->object->password);
412
413
        self::assertSame('testPMAServer', AuthenticationCookie::$authServer);
414
415
        self::assertArrayNotHasKey('pmaAuth-1', $_COOKIE);
416
    }
417
418
    public function testAuthCheckInvalidCookie(): void
419
    {
420
        Config::getInstance()->settings['AllowArbitraryServer'] = true;
421
        $_REQUEST['pma_servername'] = 'testPMAServer';
422
        $_POST['pma_password'] = 'testPMAPSWD';
423
        $_POST['pma_username'] = '';
424
        $_COOKIE['pmaUser-1'] = '';
425
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
426
427
        self::assertFalse(
428
            $this->object->readCredentials(),
429
        );
430
    }
431
432
    public function testAuthCheckExpires(): void
433
    {
434
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
435
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
436
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
437
        $_COOKIE['pmaAuth-1'] = '';
438
        $config = Config::getInstance();
439
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
440
        $_SESSION['last_access_time'] = time() - 1000;
441
        $config->settings['LoginCookieValidity'] = 1440;
442
443
        self::assertFalse(
444
            $this->object->readCredentials(),
445
        );
446
    }
447
448
    public function testAuthCheckDecryptUser(): void
449
    {
450
        $_REQUEST['old_usr'] = '';
451
        $_POST['pma_username'] = '';
452
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
453
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
454
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
455
        $config = Config::getInstance();
456
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
457
        $_SESSION['last_access_time'] = '';
458
        $config->settings['CaptchaApi'] = '';
459
        $config->settings['CaptchaRequestParam'] = '';
460
        $config->settings['CaptchaResponseParam'] = '';
461
        $config->settings['CaptchaLoginPrivateKey'] = '';
462
        $config->settings['CaptchaLoginPublicKey'] = '';
463
464
        // mock for blowfish function
465
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
466
            ->disableOriginalConstructor()
467
            ->onlyMethods(['cookieDecrypt'])
468
            ->getMock();
469
470
        $this->object->expects(self::once())
471
            ->method('cookieDecrypt')
472
            ->willReturn('testBF');
473
474
        self::assertFalse(
475
            $this->object->readCredentials(),
476
        );
477
478
        self::assertSame('testBF', $this->object->user);
479
    }
480
481
    public function testAuthCheckDecryptPassword(): void
482
    {
483
        $_REQUEST['old_usr'] = '';
484
        $_POST['pma_username'] = '';
485
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
486
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
487
        $_COOKIE['pmaAuth-1'] = 'pmaAuth1';
488
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
489
        $config = Config::getInstance();
490
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
491
        $config->settings['CaptchaApi'] = '';
492
        $config->settings['CaptchaRequestParam'] = '';
493
        $config->settings['CaptchaResponseParam'] = '';
494
        $config->settings['CaptchaLoginPrivateKey'] = '';
495
        $config->settings['CaptchaLoginPublicKey'] = '';
496
        $_SESSION['browser_access_time']['default'] = time() - 1000;
497
        $config->settings['LoginCookieValidity'] = 1440;
498
499
        // mock for blowfish function
500
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
501
            ->disableOriginalConstructor()
502
            ->onlyMethods(['cookieDecrypt'])
503
            ->getMock();
504
505
        $this->object->expects(self::exactly(2))
506
            ->method('cookieDecrypt')
507
            ->willReturn('{"password":""}');
508
509
        self::assertTrue(
510
            $this->object->readCredentials(),
511
        );
512
513
        self::assertTrue(AuthenticationCookie::$fromCookie);
514
515
        self::assertSame('', $this->object->password);
516
    }
517
518
    public function testAuthCheckAuthFails(): void
519
    {
520
        $_REQUEST['old_usr'] = '';
521
        $_POST['pma_username'] = '';
522
        $_COOKIE['pmaServer-1'] = 'pmaServ1';
523
        $_COOKIE['pmaUser-1'] = 'pmaUser1';
524
        $_COOKIE['pma_iv-1'] = base64_encode('testiv09testiv09');
525
        $config = Config::getInstance();
526
        $config->settings['blowfish_secret'] = str_repeat('a', 32);
527
        $_SESSION['last_access_time'] = 1;
528
        $config->settings['CaptchaApi'] = '';
529
        $config->settings['CaptchaRequestParam'] = '';
530
        $config->settings['CaptchaResponseParam'] = '';
531
        $config->settings['CaptchaLoginPrivateKey'] = '';
532
        $config->settings['CaptchaLoginPublicKey'] = '';
533
        $config->settings['LoginCookieValidity'] = 0;
534
        $_SESSION['browser_access_time']['default'] = -1;
535
536
        // mock for blowfish function
537
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
538
            ->disableOriginalConstructor()
539
            ->onlyMethods(['cookieDecrypt'])
540
            ->getMock();
541
542
        $this->object->expects(self::once())
543
            ->method('cookieDecrypt')
544
            ->willReturn('testBF');
545
546
        $this->expectExceptionObject(AuthenticationFailure::loggedOutDueToInactivity());
547
        $this->object->readCredentials();
548
    }
549
550
    public function testAuthSetUser(): void
551
    {
552
        $this->object->user = 'pmaUser2';
553
        $arr = ['host' => 'a', 'port' => 1, 'socket' => true, 'ssl' => true, 'user' => 'pmaUser2'];
554
555
        $config = Config::getInstance();
556
        $config->selectedServer = $arr;
557
        $config->selectedServer['user'] = 'pmaUser';
558
        $config->settings['Servers'][1] = $arr;
559
        $config->settings['AllowArbitraryServer'] = true;
560
        AuthenticationCookie::$authServer = 'b 2';
561
        $this->object->password = 'testPW';
562
        Current::$server = 2;
563
        $config->settings['LoginCookieStore'] = 100;
564
        AuthenticationCookie::$fromCookie = true;
565
566
        $this->object->storeCredentials();
567
568
        $this->object->rememberCredentials();
569
570
        self::assertArrayHasKey('pmaUser-2', $_COOKIE);
571
572
        self::assertArrayHasKey('pmaAuth-2', $_COOKIE);
573
574
        $arr['password'] = 'testPW';
575
        $arr['host'] = 'b';
576
        $arr['port'] = '2';
577
        self::assertSame($arr, $config->selectedServer);
578
    }
579
580
    public function testAuthSetUserWithHeaders(): void
581
    {
582
        $this->object->user = 'pmaUser2';
583
        $arr = ['host' => 'a', 'port' => 1, 'socket' => true, 'ssl' => true, 'user' => 'pmaUser2'];
584
585
        $config = Config::getInstance();
586
        $config->selectedServer = $arr;
587
        $config->selectedServer['host'] = 'b';
588
        $config->selectedServer['user'] = 'pmaUser';
589
        $config->settings['Servers'][1] = $arr;
590
        $config->settings['AllowArbitraryServer'] = true;
591
        $config->set('PmaAbsoluteUri', 'http://localhost/phpmyadmin');
592
        AuthenticationCookie::$authServer = 'b 2';
593
        $this->object->password = 'testPW';
594
        $config->settings['LoginCookieStore'] = 100;
595
        AuthenticationCookie::$fromCookie = false;
596
597
        $responseStub = new ResponseRendererStub();
598
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
599
600
        $this->object->storeCredentials();
601
        $response = $this->object->rememberCredentials();
602
        self::assertNotNull($response);
603
        self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode());
604
        self::assertStringEndsWith(
605
            '/phpmyadmin/index.php?route=/&db=db&table=table&lang=en',
606
            $response->getHeaderLine('Location'),
607
        );
608
    }
609
610
    public function testAuthFailsNoPass(): void
611
    {
612
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
613
            ->disableOriginalConstructor()
614
            ->onlyMethods(['showLoginForm'])
615
            ->getMock();
616
617
        $this->object->expects(self::exactly(1))
618
            ->method('showLoginForm')
619
            ->willThrowException(new ExitException());
620
621
        $_COOKIE['pmaAuth-2'] = 'pass';
622
623
        $responseStub = new ResponseRendererStub();
624
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
625
626
        try {
627
            $this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration());
628
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
629
        }
630
631
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
632
        $response = $responseStub->getResponse();
633
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
634
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
635
        self::assertSame(200, $response->getStatusCode());
636
637
        self::assertSame(
638
            'Login without a password is forbidden by configuration (see AllowNoPassword).',
639
            AuthenticationCookie::$connectionError,
640
        );
641
    }
642
643
    /** @return mixed[] */
644
    public static function dataProviderPasswordLength(): array
645
    {
646
        return [
647
            [
648
                str_repeat('a', 2001),
649
                false,
650
                'Your password is too long. To prevent denial-of-service attacks,'
651
                . ' phpMyAdmin restricts passwords to less than 2000 characters.',
652
            ],
653
            [
654
                str_repeat('a', 3000),
655
                false,
656
                'Your password is too long. To prevent denial-of-service attacks,'
657
                . ' phpMyAdmin restricts passwords to less than 2000 characters.',
658
            ],
659
            [str_repeat('a', 256), true, ''],
660
            ['', true, ''],
661
        ];
662
    }
663
664
    #[DataProvider('dataProviderPasswordLength')]
665
    public function testAuthFailsTooLongPass(string $password, bool $expected, string $connError): void
666
    {
667
        $_POST['pma_username'] = str_shuffle('123456987rootfoobar');
668
        $_POST['pma_password'] = $password;
669
670
        self::assertSame(
671
            $expected,
672
            $this->object->readCredentials(),
673
        );
674
675
        self::assertSame($connError, AuthenticationCookie::$connectionError);
676
    }
677
678
    public function testAuthFailsDeny(): void
679
    {
680
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
681
            ->disableOriginalConstructor()
682
            ->onlyMethods(['showLoginForm'])
683
            ->getMock();
684
685
        $this->object->expects(self::exactly(1))
686
            ->method('showLoginForm')
687
            ->willThrowException(new ExitException());
688
689
        $_COOKIE['pmaAuth-2'] = 'pass';
690
691
        $responseStub = new ResponseRendererStub();
692
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
693
694
        try {
695
            $this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules());
696
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
697
        }
698
699
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
700
        $response = $responseStub->getResponse();
701
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
702
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
703
        self::assertSame(200, $response->getStatusCode());
704
705
        self::assertSame('Access denied!', AuthenticationCookie::$connectionError);
706
    }
707
708
    public function testAuthFailsActivity(): void
709
    {
710
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
711
            ->disableOriginalConstructor()
712
            ->onlyMethods(['showLoginForm'])
713
            ->getMock();
714
715
        $this->object->expects(self::exactly(1))
716
            ->method('showLoginForm')
717
            ->willThrowException(new ExitException());
718
719
        $_COOKIE['pmaAuth-2'] = 'pass';
720
721
        Config::getInstance()->settings['LoginCookieValidity'] = 10;
722
723
        $responseStub = new ResponseRendererStub();
724
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
725
726
        try {
727
            $this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity());
728
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
729
        }
730
731
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
732
        $response = $responseStub->getResponse();
733
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
734
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
735
        self::assertSame(200, $response->getStatusCode());
736
737
        self::assertSame(
738
            'You have been automatically logged out due to inactivity of 10 seconds.'
739
            . ' Once you log in again, you should be able to resume the work where you left off.',
740
            AuthenticationCookie::$connectionError,
741
        );
742
    }
743
744
    public function testAuthFailsDBI(): void
745
    {
746
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
747
            ->disableOriginalConstructor()
748
            ->onlyMethods(['showLoginForm'])
749
            ->getMock();
750
751
        $this->object->expects(self::exactly(1))
752
            ->method('showLoginForm')
753
            ->willThrowException(new ExitException());
754
755
        $_COOKIE['pmaAuth-2'] = 'pass';
756
757
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
758
            ->disableOriginalConstructor()
759
            ->getMock();
760
761
        $dbi->expects(self::once())
762
            ->method('getError')
763
            ->willReturn('');
764
765
        DatabaseInterface::$instance = $dbi;
766
        DatabaseInterface::$errorNumber = 42;
767
768
        $responseStub = new ResponseRendererStub();
769
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
770
771
        try {
772
            $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
773
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
774
        }
775
776
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
777
        $response = $responseStub->getResponse();
778
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
779
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
780
        self::assertSame(200, $response->getStatusCode());
781
782
        self::assertSame('#42 Cannot log in to the database server.', AuthenticationCookie::$connectionError);
783
    }
784
785
    public function testAuthFailsErrno(): void
786
    {
787
        $this->object = $this->getMockBuilder(AuthenticationCookie::class)
788
            ->disableOriginalConstructor()
789
            ->onlyMethods(['showLoginForm'])
790
            ->getMock();
791
792
        $this->object->expects(self::exactly(1))
793
            ->method('showLoginForm')
794
            ->willThrowException(new ExitException());
795
796
        $dbi = $this->getMockBuilder(DatabaseInterface::class)
797
            ->disableOriginalConstructor()
798
            ->getMock();
799
800
        $dbi->expects(self::once())
801
            ->method('getError')
802
            ->willReturn('');
803
804
        DatabaseInterface::$instance = $dbi;
805
        $_COOKIE['pmaAuth-2'] = 'pass';
806
807
        DatabaseInterface::$errorNumber = null;
808
809
        $responseStub = new ResponseRendererStub();
810
        (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub);
811
812
        try {
813
            $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer());
814
        } catch (Throwable $throwable) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
815
        }
816
817
        self::assertInstanceOf(ExitException::class, $throwable ?? null);
818
        $response = $responseStub->getResponse();
819
        self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control'));
820
        self::assertSame(['no-cache'], $response->getHeader('Pragma'));
821
        self::assertSame(200, $response->getStatusCode());
822
823
        self::assertSame('Cannot log in to the database server.', AuthenticationCookie::$connectionError);
824
    }
825
826
    public function testGetEncryptionSecretEmpty(): void
827
    {
828
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
829
830
        Config::getInstance()->settings['blowfish_secret'] = '';
831
        $_SESSION['encryption_key'] = '';
832
833
        $result = $method->invoke($this->object, null);
834
835
        self::assertSame($result, $_SESSION['encryption_key']);
836
        self::assertSame(SODIUM_CRYPTO_SECRETBOX_KEYBYTES, mb_strlen($result, '8bit'));
837
    }
838
839
    public function testGetEncryptionSecretConfigured(): void
840
    {
841
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
842
843
        $key = str_repeat('a', SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
844
        Config::getInstance()->settings['blowfish_secret'] = $key;
845
        $_SESSION['encryption_key'] = '';
846
847
        $result = $method->invoke($this->object, null);
848
849
        self::assertSame($key, $result);
850
    }
851
852
    public function testGetSessionEncryptionSecretConfigured(): void
853
    {
854
        $method = new ReflectionMethod(AuthenticationCookie::class, 'getEncryptionSecret');
855
856
        $key = str_repeat('a', SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
857
        Config::getInstance()->settings['blowfish_secret'] = 'blowfish_secret';
858
        $_SESSION['encryption_key'] = $key;
859
860
        $result = $method->invoke($this->object, null);
861
862
        self::assertSame($key, $result);
863
    }
864
865
    public function testCookieEncryption(): void
866
    {
867
        $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
868
        $encrypted = $this->object->cookieEncrypt('data123', $key);
869
        self::assertNotFalse(base64_decode($encrypted, true));
870
        self::assertSame('data123', $this->object->cookieDecrypt($encrypted, $key));
871
    }
872
873
    public function testCookieDecryptInvalid(): void
874
    {
875
        self::assertNull($this->object->cookieDecrypt('', ''));
876
877
        $key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
878
        $encrypted = $this->object->cookieEncrypt('data123', $key);
879
        self::assertSame('data123', $this->object->cookieDecrypt($encrypted, $key));
880
881
        self::assertNull($this->object->cookieDecrypt('', $key));
882
        self::assertNull($this->object->cookieDecrypt($encrypted, ''));
883
        self::assertNull($this->object->cookieDecrypt($encrypted, random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES)));
884
    }
885
886
    /** @throws ReflectionException */
887
    public function testPasswordChange(): void
888
    {
889
        $newPassword = 'PMAPASSWD2';
890
        $config = Config::getInstance();
891
        $config->settings['AllowArbitraryServer'] = true;
892
        AuthenticationCookie::$authServer = 'b 2';
893
        $_SESSION['encryption_key'] = '';
894
        $_COOKIE = [];
895
896
        $this->object->handlePasswordChange($newPassword);
897
898
        $payload = ['password' => $newPassword, 'server' => 'b 2'];
899
900
        /** @psalm-suppress EmptyArrayAccess */
901
        self::assertIsString($_COOKIE['pmaAuth-' . Current::$server]);
902
        $decryptedCookie = $this->object->cookieDecrypt(
903
            $_COOKIE['pmaAuth-' . Current::$server],
904
            $_SESSION['encryption_key'],
905
        );
906
        self::assertSame(json_encode($payload), $decryptedCookie);
907
    }
908
909
    public function testAuthenticate(): void
910
    {
911
        $config = Config::getInstance();
912
        $config->settings['CaptchaApi'] = '';
913
        $config->settings['CaptchaRequestParam'] = '';
914
        $config->settings['CaptchaResponseParam'] = '';
915
        $config->settings['CaptchaLoginPrivateKey'] = '';
916
        $config->settings['CaptchaLoginPublicKey'] = '';
917
        $config->selectedServer['AllowRoot'] = false;
918
        $config->selectedServer['AllowNoPassword'] = false;
919
        $_REQUEST['old_usr'] = '';
920
        $_POST['pma_username'] = 'testUser';
921
        $_POST['pma_password'] = 'testPassword';
922
923
        ob_start();
924
        $response = $this->object->authenticate();
925
        $result = ob_get_clean();
926
927
        self::assertNull($response);
928
        /* Nothing should be printed */
929
        self::assertSame('', $result);
930
931
        /* Verify readCredentials worked */
932
        self::assertSame('testUser', $this->object->user);
933
        self::assertSame('testPassword', $this->object->password);
934
935
        /* Verify storeCredentials worked */
936
        self::assertSame('testUser', $config->selectedServer['user']);
937
        self::assertSame('testPassword', $config->selectedServer['password']);
938
    }
939
940
    /**
941
     * @param string  $user     user
942
     * @param string  $pass     pass
943
     * @param string  $ip       ip
944
     * @param bool    $root     root
945
     * @param bool    $nopass   nopass
946
     * @param mixed[] $rules    rules
947
     * @param string  $expected expected result
948
     */
949
    #[DataProvider('checkRulesProvider')]
950
    public function testCheckRules(
951
        string $user,
952
        string $pass,
953
        string $ip,
954
        bool $root,
955
        bool $nopass,
956
        array $rules,
957
        string $expected,
958
    ): void {
959
        $this->object->user = $user;
960
        $this->object->password = $pass;
961
        $this->object->storeCredentials();
962
963
        $_SERVER['REMOTE_ADDR'] = $ip;
964
965
        $config = Config::getInstance();
966
        $config->selectedServer['AllowRoot'] = $root;
967
        $config->selectedServer['AllowNoPassword'] = $nopass;
968
        $config->selectedServer['AllowDeny'] = $rules;
969
970
        $exception = null;
971
        try {
972
            $this->object->checkRules();
973
        } catch (AuthenticationFailure $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
974
        }
975
976
        if ($expected === '') {
977
            self::assertNull($exception, 'checkRules() should not throw an exception.');
978
979
            return;
980
        }
981
982
        self::assertInstanceOf(AuthenticationFailure::class, $exception);
983
        self::assertSame($expected, $exception->failureType);
984
    }
985
986
    /** @return mixed[] */
987
    public static function checkRulesProvider(): array
988
    {
989
        return [
990
            'nopass-ok' => ['testUser', '', '1.2.3.4', true, true, [], ''],
991
            'nopass' => ['testUser', '', '1.2.3.4', true, false, [], AuthenticationFailure::EMPTY_DENIED],
992
            'root-ok' => ['root', 'root', '1.2.3.4', true, true, [], ''],
993
            'root' => ['root', 'root', '1.2.3.4', false, true, [], AuthenticationFailure::ROOT_DENIED],
994
            'rules-deny-allow-ok' => [
995
                'root',
996
                'root',
997
                '1.2.3.4',
998
                true,
999
                true,
1000
                ['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']],
1001
                '',
1002
            ],
1003
            'rules-deny-allow-reject' => [
1004
                'user',
1005
                'root',
1006
                '1.2.3.4',
1007
                true,
1008
                true,
1009
                ['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']],
1010
                AuthenticationFailure::ALLOW_DENIED,
1011
            ],
1012
            'rules-allow-deny-ok' => [
1013
                'root',
1014
                'root',
1015
                '1.2.3.4',
1016
                true,
1017
                true,
1018
                ['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1019
                '',
1020
            ],
1021
            'rules-allow-deny-reject' => [
1022
                'user',
1023
                'root',
1024
                '1.2.3.4',
1025
                true,
1026
                true,
1027
                ['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1028
                AuthenticationFailure::ALLOW_DENIED,
1029
            ],
1030
            'rules-explicit-ok' => [
1031
                'root',
1032
                'root',
1033
                '1.2.3.4',
1034
                true,
1035
                true,
1036
                ['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1037
                '',
1038
            ],
1039
            'rules-explicit-reject' => [
1040
                'user',
1041
                'root',
1042
                '1.2.3.4',
1043
                true,
1044
                true,
1045
                ['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']],
1046
                AuthenticationFailure::ALLOW_DENIED,
1047
            ],
1048
        ];
1049
    }
1050
}
1051