Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.94% covered (success)
93.94%
31 / 33
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
KeycloakProviderConfigFactory
93.94% covered (success)
93.94%
31 / 33
50.00% covered (danger)
50.00%
2 / 4
16.06
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
 create
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
8
 buildDiscoveryUrl
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 toBool
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3/**
4 * Keycloak OIDC provider config factory.
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-18
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Auth\Keycloak;
21
22use InvalidArgumentException;
23use phpMyFAQ\Auth\Oidc\OidcClientConfig;
24use phpMyFAQ\Auth\Oidc\OidcProviderConfig;
25use phpMyFAQ\Configuration;
26
27final readonly class KeycloakProviderConfigFactory
28{
29    public function __construct(
30        private Configuration $configuration,
31    ) {
32    }
33
34    public function create(): OidcProviderConfig
35    {
36        $baseUrl = rtrim(trim((string) $this->configuration->get('keycloak.baseUrl')), characters: '/');
37        $realm = trim((string) $this->configuration->get('keycloak.realm'));
38        $redirectUri = trim((string) $this->configuration->get('keycloak.redirectUri'));
39        $scopes = preg_split('/\s+/', trim((string) $this->configuration->get('keycloak.scopes')));
40        if ($scopes === false) {
41            $scopes = [];
42        }
43
44        $enabled = $this->toBool($this->configuration->get('keycloak.enable'));
45
46        if ($enabled && ($baseUrl === '' || $realm === '')) {
47            $missing = array_filter([
48                $baseUrl === '' ? 'baseUrl' : null,
49                $realm === '' ? 'realm' : null,
50            ]);
51            throw new InvalidArgumentException(sprintf('Keycloak enabled but missing: %s', implode(' and ', $missing)));
52        }
53
54        if ($redirectUri === '') {
55            $redirectUri = rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/auth/keycloak/callback';
56        }
57
58        return new OidcProviderConfig(
59            provider: 'keycloak',
60            enabled: $enabled,
61            discoveryUrl: $this->buildDiscoveryUrl($baseUrl, $realm),
62            client: new OidcClientConfig(
63                clientId: trim((string) $this->configuration->get('keycloak.clientId')),
64                clientSecret: (string) $this->configuration->get('keycloak.clientSecret'),
65                redirectUri: $redirectUri,
66                scopes: array_values(array_filter($scopes, static fn(string $scope): bool => $scope !== '')),
67            ),
68            autoProvision: $this->toBool($this->configuration->get('keycloak.autoProvision')),
69            logoutRedirectUrl: trim((string) $this->configuration->get('keycloak.logoutRedirectUrl')),
70        );
71    }
72
73    private function buildDiscoveryUrl(string $baseUrl, string $realm): string
74    {
75        if ($baseUrl === '' || $realm === '') {
76            return '';
77        }
78
79        return $baseUrl . '/realms/' . rawurlencode($realm) . '/.well-known/openid-configuration';
80    }
81
82    private function toBool(mixed $value): bool
83    {
84        return $value === true || $value === 1 || $value === '1' || $value === 'true';
85    }
86}