Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.37% covered (success)
97.37%
37 / 38
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClientRepository
97.37% covered (success)
97.37%
37 / 38
50.00% covered (danger)
50.00%
1 / 2
13
0.00% covered (danger)
0.00%
0 / 1
 getClientEntity
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
5
 validateClient
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3/**
4 * OAuth2 client repository.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-02-09
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Auth\OAuth2\Repository;
21
22use League\OAuth2\Server\Entities\ClientEntityInterface;
23use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
24use phpMyFAQ\Auth\OAuth2\Entity\ClientEntity;
25
26final class ClientRepository extends AbstractRepository implements ClientRepositoryInterface
27{
28    public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface
29    {
30        $query = sprintf(
31            "SELECT client_id, client_secret, name, redirect_uri, grants, is_confidential
32             FROM %s
33             WHERE client_id = '%s'",
34            $this->table('faqoauth_clients'),
35            $this->db()->escape($clientIdentifier),
36        );
37
38        $result = $this->db()->query($query);
39        if ($result === false) {
40            return null;
41        }
42
43        $row = $this->db()->fetchObject($result);
44        if (!is_object($row)) {
45            return null;
46        }
47
48        $clientIdentifier = (string) $row->client_id;
49        if ($clientIdentifier === '') {
50            return null;
51        }
52
53        $entity = new ClientEntity();
54        $entity->setIdentifier($clientIdentifier);
55        $entity->secret = $row->client_secret !== null ? (string) $row->client_secret : null;
56        $entity->setName((string) ($row->name ?? $row->client_id));
57        $entity->setRedirectUri((string) ($row->redirect_uri ?? ''));
58        $entity->setConfidential((int) ($row->is_confidential ?? 1) === 1);
59
60        $grants = array_filter(array_map('trim', explode(',', (string) ($row->grants ?? ''))));
61        $entity->allowedGrants = array_values($grants);
62
63        return $entity;
64    }
65
66    public function validateClient(
67        string $clientIdentifier,
68        #[\SensitiveParameter]
69        ?string $clientSecret,
70        ?string $grantType,
71    ): bool {
72        $client = $this->getClientEntity($clientIdentifier);
73        if (!$client instanceof ClientEntity) {
74            return false;
75        }
76
77        if ($grantType !== null && !$client->supportsGrantType($grantType)) {
78            return false;
79        }
80
81        if (!$client->isConfidential()) {
82            return true;
83        }
84
85        $storedSecret = $client->secret;
86        if ($storedSecret === null || $clientSecret === null) {
87            return false;
88        }
89
90        $passwordInfo = password_get_info($storedSecret);
91        if (($passwordInfo['algoName'] ?? 'unknown') !== 'unknown') {
92            return password_verify($clientSecret, $storedSecret);
93        }
94
95        return hash_equals($storedSecret, $clientSecret);
96    }
97}