Skip to content

Commit c9d8775

Browse files
committed
feat(admin): added OAuth settings in admin configuration
1 parent e4908ac commit c9d8775

20 files changed

Lines changed: 636 additions & 234 deletions

.docker/php-fpm/docker-entrypoint.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
# Exit on error
44
set -e
55

6+
#=== Provide backward-compatible project root for Composer PSR-4 paths ===
7+
# The generated autoloader expects /var/www/phpmyfaq while the development
8+
# stack mounts the application at /var/www/html.
9+
if [ ! -e "/var/www/phpmyfaq" ]; then
10+
ln -s /var/www/html /var/www/phpmyfaq
11+
fi
12+
613
#=== Set folder permissions ===
714
folders="content/core/config content/core/data content/core/logs content/user/attachments content/user/images"
815

docs/configuration.md

Lines changed: 211 additions & 199 deletions
Large diffs are not rendered by default.

phpmyfaq/admin/assets/src/configuration/configuration.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,18 @@ describe('Configuration Functions', () => {
288288
<li class="nav-item" data-config-group="core" data-config-label="Main">
289289
<a href="#main" data-bs-toggle="tab"></a>
290290
</li>
291-
<li class="nav-item" data-config-group="appearance" data-config-label="Layout">
292-
<a href="#layout" data-bs-toggle="tab"></a>
293-
</li>
294-
</ul>
291+
<li class="nav-item" data-config-group="appearance" data-config-label="Layout">
292+
<a href="#layout" data-bs-toggle="tab"></a>
293+
</li>
294+
<li class="nav-item" data-config-group="integrations" data-config-label="OAuth 2.0">
295+
<a href="#oauth2" data-bs-toggle="tab"></a>
296+
</li>
297+
</ul>
295298
</form>
296299
<div id="pmf-configuration-result"></div>
297300
<input id="pmf-language" value="en">
298301
<div id="main"></div>
302+
<div id="oauth2"></div>
299303
`;
300304

301305
(fetchConfiguration as Mock).mockResolvedValue('Configuration content');
@@ -339,6 +343,10 @@ describe('Configuration Functions', () => {
339343
<div class="pmf-config-item" data-config-key="mail.remoteSMTPPassword">SMTP password</div>
340344
</div>
341345
<div id="api"></div>
346+
<div id="oauth2">
347+
<div class="pmf-config-item" data-config-key="oauth2.enable">Enable OAuth 2.0 authorization server</div>
348+
<div class="pmf-config-item" data-config-key="oauth2.privateKeyPath">Private key path</div>
349+
</div>
342350
<div id="upgrade">
343351
<div class="pmf-config-item" data-config-key="upgrade.releaseEnvironment">Release environment</div>
344352
</div>

phpmyfaq/admin/assets/src/configuration/configuration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const TAB_TARGETS = [
4747
'#layout',
4848
'#mail',
4949
'#api',
50+
'#oauth2',
5051
'#upgrade',
5152
'#translation',
5253
'#storage',

phpmyfaq/assets/templates/admin/configuration/main.twig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@
119119
API
120120
</a>
121121
</li>
122+
<li role="presentation" class="nav-item" data-config-group="integrations" data-config-label="{{ 'oauthControlCenter' | translate }}">
123+
<a href="#oauth2" aria-controls="oauth2" role="tab" data-bs-toggle="tab" class="nav-link">
124+
<i aria-hidden="true" class="bi bi-shield-lock"></i>
125+
{{ 'oauthControlCenter' | translate }}
126+
</a>
127+
</li>
122128
<li role="presentation" class="nav-item" data-config-group="integrations" data-config-label="LDAP">
123129
<a href="#ldap" aria-controls="ldap" role="tab" data-bs-toggle="tab" class="nav-link">
124130
<i aria-hidden="true" class="bi bi-database"></i>
@@ -154,6 +160,7 @@
154160
<div role="tabpanel" class="tab-pane fade" id="layout"></div>
155161
<div role="tabpanel" class="tab-pane fade" id="mail"></div>
156162
<div role="tabpanel" class="tab-pane fade" id="api"></div>
163+
<div role="tabpanel" class="tab-pane fade" id="oauth2"></div>
157164
<div role="tabpanel" class="tab-pane fade" id="upgrade"></div>
158165
<div role="tabpanel" class="tab-pane fade" id="translation"></div>
159166
<div role="tabpanel" class="tab-pane fade" id="storage"></div>

phpmyfaq/assets/templates/admin/configuration/tab-list.twig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
'mail': 'mailControlCenter',
1414
'push': 'pushControlCenter',
1515
'api': 'API',
16+
'oauth2': 'oauthControlCenter',
1617
'ldap': 'LDAP',
1718
'upgrade': 'upgradeControlCenter',
1819
} %}

phpmyfaq/src/phpMyFAQ/Api/MetaService.php

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
{
2727
public function __construct(
2828
private Configuration $configuration,
29+
private OAuthDiscoveryService $oAuthDiscoveryService,
2930
) {
3031
}
3132

@@ -49,7 +50,7 @@ public function getPublicMetadata(): array
4950
'availableLanguages' => LanguageHelper::getAvailableLanguages(),
5051
'enabledFeatures' => $this->buildEnabledFeatures(),
5152
'publicLogoUrl' => $this->buildPublicLogoUrl(),
52-
'oauthDiscovery' => $this->buildOAuthDiscovery(),
53+
'oauthDiscovery' => $this->oAuthDiscoveryService->getMetaDiscovery(),
5354
];
5455
}
5556

@@ -70,24 +71,6 @@ private function buildEnabledFeatures(): array
7071
];
7172
}
7273

73-
/**
74-
* @return array<string, bool|string|string[]>
75-
*/
76-
private function buildOAuthDiscovery(): array
77-
{
78-
$apiBaseUrl = rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/api';
79-
80-
return [
81-
'enabled' => $this->toBool($this->configuration->get('oauth2.enable')),
82-
'issuer' => $apiBaseUrl,
83-
'authorizationEndpoint' => $apiBaseUrl . '/oauth/authorize',
84-
'tokenEndpoint' => $apiBaseUrl . '/oauth/token',
85-
'grantTypesSupported' => ['authorization_code', 'client_credentials', 'refresh_token'],
86-
'responseTypesSupported' => ['code'],
87-
'tokenEndpointAuthMethodsSupported' => ['client_secret_basic', 'client_secret_post', 'none'],
88-
];
89-
}
90-
9174
private function buildPublicLogoUrl(): string
9275
{
9376
return rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/assets/images/logo-transparent.svg';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/**
4+
* OAuth discovery metadata service.
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-04-11
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Api;
21+
22+
use phpMyFAQ\Configuration;
23+
24+
final readonly class OAuthDiscoveryService
25+
{
26+
public function __construct(
27+
private Configuration $configuration,
28+
) {
29+
}
30+
31+
/**
32+
* @return array<string, bool|string|string[]>
33+
*/
34+
public function getMetaDiscovery(): array
35+
{
36+
$document = $this->getDiscoveryDocument();
37+
38+
return [
39+
'enabled' => $this->isEnabled(),
40+
'issuer' => (string) $document['issuer'],
41+
'authorizationEndpoint' => (string) $document['authorization_endpoint'],
42+
'tokenEndpoint' => (string) $document['token_endpoint'],
43+
'grantTypesSupported' => $document['grant_types_supported'],
44+
'responseTypesSupported' => $document['response_types_supported'],
45+
'tokenEndpointAuthMethodsSupported' => $document['token_endpoint_auth_methods_supported'],
46+
];
47+
}
48+
49+
/**
50+
* @return array<string, string|string[]>
51+
*/
52+
public function getDiscoveryDocument(): array
53+
{
54+
$apiBaseUrl = rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/api';
55+
56+
return [
57+
'issuer' => $apiBaseUrl,
58+
'authorization_endpoint' => $apiBaseUrl . '/oauth/authorize',
59+
'token_endpoint' => $apiBaseUrl . '/oauth/token',
60+
'grant_types_supported' => ['authorization_code', 'client_credentials', 'refresh_token'],
61+
'response_types_supported' => ['code'],
62+
'token_endpoint_auth_methods_supported' => ['client_secret_basic', 'client_secret_post', 'none'],
63+
];
64+
}
65+
66+
public function isEnabled(): bool
67+
{
68+
$value = $this->configuration->get('oauth2.enable');
69+
70+
return $value === true || $value === 1 || $value === '1' || $value === 'true';
71+
}
72+
}

phpmyfaq/src/phpMyFAQ/Controller/Api/MetaController.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
use OpenApi\Attributes as OA;
2323
use phpMyFAQ\Api\MetaService;
24+
use phpMyFAQ\Api\OAuthDiscoveryService;
2425
use phpMyFAQ\Controller\AbstractController;
2526
use Symfony\Component\HttpFoundation\JsonResponse;
2627
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
@@ -33,7 +34,10 @@ final class MetaController extends AbstractController
3334
public function __construct(?MetaService $metaService = null)
3435
{
3536
parent::__construct();
36-
$this->metaService = $metaService ?? new MetaService($this->configuration);
37+
$this->metaService = $metaService ?? new MetaService(
38+
$this->configuration,
39+
new OAuthDiscoveryService($this->configuration),
40+
);
3741

3842
if (!$this->isApiEnabled()) {
3943
throw new UnauthorizedHttpException(challenge: 'API is not enabled');
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/**
4+
* OAuth discovery endpoint controller.
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-04-11
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
namespace phpMyFAQ\Controller\Frontend;
21+
22+
use OpenApi\Attributes as OA;
23+
use phpMyFAQ\Api\OAuthDiscoveryService;
24+
use phpMyFAQ\Controller\AbstractController;
25+
use Symfony\Component\HttpFoundation\JsonResponse;
26+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
27+
use Symfony\Component\Routing\Attribute\Route;
28+
29+
final class OAuthDiscoveryController extends AbstractController
30+
{
31+
public function __construct(
32+
private readonly OAuthDiscoveryService $oAuthDiscoveryService,
33+
) {
34+
parent::__construct();
35+
}
36+
37+
#[OA\Get(
38+
path: '/.well-known/oauth-authorization-server',
39+
operationId: 'getOAuthAuthorizationServerMetadata',
40+
tags: ['Public Endpoints'],
41+
)]
42+
#[OA\Response(
43+
response: 200,
44+
description: 'Returns OAuth 2.0 Authorization Server Metadata.',
45+
content: new OA\JsonContent(properties: [
46+
new OA\Property(property: 'issuer', type: 'string', example: 'https://localhost/api'),
47+
new OA\Property(property: 'authorization_endpoint', type: 'string'),
48+
new OA\Property(property: 'token_endpoint', type: 'string'),
49+
new OA\Property(property: 'grant_types_supported', type: 'array', items: new OA\Items(type: 'string')),
50+
new OA\Property(property: 'response_types_supported', type: 'array', items: new OA\Items(type: 'string')),
51+
new OA\Property(
52+
property: 'token_endpoint_auth_methods_supported',
53+
type: 'array',
54+
items: new OA\Items(type: 'string'),
55+
),
56+
]),
57+
)]
58+
#[Route(path: '/.well-known/oauth-authorization-server', name: 'public.oauth.discovery', methods: ['GET'])]
59+
public function index(): JsonResponse
60+
{
61+
if (!$this->oAuthDiscoveryService->isEnabled()) {
62+
throw new NotFoundHttpException('OAuth 2.0 authorization server metadata is not available.');
63+
}
64+
65+
return $this->json($this->oAuthDiscoveryService->getDiscoveryDocument());
66+
}
67+
}

0 commit comments

Comments
 (0)