Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.47% covered (warning)
76.47%
13 / 17
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
HibpCompromisedPasswordValidator
75.00% covered (warning)
75.00%
12 / 16
50.00% covered (danger)
50.00%
1 / 2
6.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
5.47
 isCompromised
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Security\Password;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Integrations\Application\Service\CompromisedPasswordCheckService;
12use App\Modules\Integrations\Infrastructure\Provider\HaveIBeenPwnedProvider;
13use App\Modules\Integrations\Infrastructure\Security\SsrfProtectionClientInterface;
14use Closure;
15use PDO;
16
17/**
18 * HaveIBeenPwned & Offline Hybrid Compromised Password Validator.
19 *
20 * Implements NIST SP 800-63B ยง5.1.1.2 and OWASP ASVS v5 V2.1.7 by delegating password
21 * breach verification to the registered HaveIBeenPwned security integration service.
22 *
23 * @package App\Core\Security\Password
24 */
25final readonly class HibpCompromisedPasswordValidator implements CompromisedPasswordValidatorInterface
26{
27    private CompromisedPasswordCheckService $checkService;
28
29    /**
30     * HibpCompromisedPasswordValidator constructor.
31     *
32     * @param CompromisedPasswordCheckService|Closure|null $serviceOrFetcher Custom check service or fetcher closure.
33     * @param (Closure(string): ?string)|null              $httpFetcher      Alternative HTTP fetcher for testing.
34     * @param PDO|null                                     $pdo              Database connection instance.
35     */
36    public function __construct(
37        CompromisedPasswordCheckService|Closure|null $serviceOrFetcher = null,
38        ?Closure $httpFetcher = null,
39        ?PDO $pdo = null
40    ) {
41        if ($serviceOrFetcher instanceof CompromisedPasswordCheckService) {
42            $this->checkService = $serviceOrFetcher;
43            return;
44        }
45
46        $effectiveFetcher = $serviceOrFetcher instanceof Closure ? $serviceOrFetcher : $httpFetcher;
47        if ($effectiveFetcher !== null) {
48            $client = new class ($effectiveFetcher) implements SsrfProtectionClientInterface {
49                public function __construct(private readonly Closure $fetcher)
50                {
51                }
52
53                public function get(string $url, array $headers = [], int $timeout = 3): array
54                {
55                    $body = ($this->fetcher)($url);
56                    return ['status' => $body !== null ? 200 : 500, 'body' => (string) $body];
57                }
58
59                public function post(string $url, array $headers = [], ?string $body = null, int $timeout = 3): array
60                {
61                    return ['status' => 500, 'body' => ''];
62                }
63
64                public function request(
65                    string $url,
66                    string $method = 'GET',
67                    array $headers = [],
68                    ?string $body = null,
69                    int $timeout = 3
70                ): array {
71                    return $this->get($url, $headers, $timeout);
72                }
73            };
74
75            $provider = new HaveIBeenPwnedProvider($client);
76            $this->checkService = new CompromisedPasswordCheckService($pdo, $provider);
77        } else {
78            $this->checkService = new CompromisedPasswordCheckService($pdo);
79        }
80    }
81
82    /**
83     * {@inheritdoc}
84     */
85    public function isCompromised(string $password): bool
86    {
87        return $this->checkService->isCompromised($password);
88    }
89}