Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.74% covered (warning)
76.74%
33 / 43
83.33% covered (success)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceServer
76.74% covered (warning)
76.74%
33 / 43
83.33% covered (success)
83.33%
5 / 6
25.03
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTokenValidator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 authenticate
58.33% covered (warning)
58.33%
14 / 24
0.00% covered (danger)
0.00%
0 / 1
14.86
 isEnabled
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
4
 getConfigString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 toPsr7Request
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3/**
4 * OAuth2 resource server facade for bearer-token authentication.
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;
21
22use GuzzleHttp\Psr7\ServerRequest as Psr7ServerRequest;
23use League\OAuth2\Server\CryptKey;
24use League\OAuth2\Server\ResourceServer as LeagueResourceServer;
25use phpMyFAQ\Auth\OAuth2\Repository\AccessTokenRepository;
26use phpMyFAQ\Configuration;
27use Symfony\Component\HttpFoundation\Request;
28
29final class ResourceServer
30{
31    /** @var callable(Request): ?int|null */
32    private $tokenValidator = null;
33
34    public function __construct(
35        private readonly Configuration $configuration,
36    ) {
37    }
38
39    /**
40     * Registers OAuth2 token validation logic.
41     *
42     * @param callable(Request): ?int $validator
43     */
44    public function setTokenValidator(callable $validator): void
45    {
46        $this->tokenValidator = $validator;
47    }
48
49    /**
50     * Authenticates bearer tokens and returns the authenticated user ID if available.
51     */
52    public function authenticate(Request $request): ?int
53    {
54        $authorizationHeader = trim((string) $request->headers->get('Authorization', ''));
55        if (!str_starts_with($authorizationHeader, 'Bearer ')) {
56            return null;
57        }
58
59        if ($this->tokenValidator !== null) {
60            return ($this->tokenValidator)($request);
61        }
62
63        if (!class_exists(\League\OAuth2\Server\ResourceServer::class)) {
64            return null;
65        }
66
67        if (!$this->isEnabled()) {
68            return null;
69        }
70
71        $publicKeyPath = $this->getConfigString('oauth2.publicKeyPath');
72        if ($publicKeyPath === '') {
73            return null;
74        }
75
76        try {
77            $psrRequest = $this->toPsr7Request($request);
78            $server = new LeagueResourceServer(
79                new AccessTokenRepository($this->configuration),
80                new CryptKey($publicKeyPath),
81            );
82            $validatedRequest = $server->validateAuthenticatedRequest($psrRequest);
83            $userId = $validatedRequest->getAttribute('oauth_user_id');
84
85            if ($userId === null || $userId === '') {
86                return null;
87            }
88
89            return (int) $userId;
90        } catch (\Throwable) {
91            return null;
92        }
93    }
94
95    private function isEnabled(): bool
96    {
97        $value = $this->configuration->get('oauth2.enable');
98        return $value === true || $value === 'true' || $value === 1 || $value === '1';
99    }
100
101    private function getConfigString(string $key): string
102    {
103        $value = $this->configuration->get($key);
104        return is_string($value) ? trim($value) : '';
105    }
106
107    private function toPsr7Request(Request $request): Psr7ServerRequest
108    {
109        $headers = [];
110        foreach ($request->headers->all() as $name => $values) {
111            $headers[$name] = implode(', ', $values);
112        }
113
114        $psrRequest = new Psr7ServerRequest(
115            $request->getMethod(),
116            $request->getUri(),
117            $headers,
118            $request->getContent(),
119        );
120
121        $parsedBody = $request->request->all();
122        if ($parsedBody !== []) {
123            $psrRequest = $psrRequest->withParsedBody($parsedBody);
124        }
125
126        return $psrRequest->withQueryParams($request->query->all());
127    }
128}