diff --git a/.gitignore b/.gitignore index 4c8afd189c..3fd781ff2c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Builds build/* +site/ # Docker persistent data volumes/* diff --git a/.prettierignore b/.prettierignore index 9848a45899..2dffeffe52 100644 --- a/.prettierignore +++ b/.prettierignore @@ -17,7 +17,7 @@ pnpm-lock.yaml # Ignore artifacts phpmyfaq/assets/public/ +site/ # Ignore PNPM Cache .pnpm-store - diff --git a/CHANGELOG.md b/CHANGELOG.md index b37f6dd629..12d6d34292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ This is a log of major user-visible changes in each phpMyFAQ release. - added optional Redis support for configuration caching (Thorsten) - added LDAP configuration frontend (Thorsten) - added phpMyFAQ recent news widget to the admin dashboard (Thorsten) +- added experimental support for Keycloak (Thorsten) - added experimental support for API key authentication via OAuth2 (Thorsten) - added experimental per-tenant quota enforcement and API request rate limits (Thorsten) - added SBOM (Software Bill of Materials) generation (Thorsten) diff --git a/composer.json b/composer.json index 497577392a..a64f287aea 100644 --- a/composer.json +++ b/composer.json @@ -135,6 +135,7 @@ "test:coverage": "./phpmyfaq/src/libs/bin/phpunit --coverage-text", "test:coverage-html": "./phpmyfaq/src/libs/bin/phpunit --coverage-html coverage", "bench": "./phpmyfaq/src/libs/bin/phpbench run tests/Benchmarks --report=aggregate", + "docs:build": "mkdocs build --strict", "version:get": "php scripts/get-version.php", "version:sync": "pnpm version $(php scripts/get-version.php) --no-git-tag-version --allow-same-version", "version:check": "php scripts/check-version.php" diff --git a/docs/administration.md b/docs/administration.md index d10909167a..817556db3e 100644 --- a/docs/administration.md +++ b/docs/administration.md @@ -499,7 +499,7 @@ Navigate to the **Configuration → Translation** tab to configure your translat - **Amazon**: AWS Access Key ID, Secret Access Key, and region - **LibreTranslate**: Server URL and optional API key -For detailed setup instructions for each provider, see the [AI Translation Guide](9-ai-translation.md). +For detailed setup instructions for each provider, see the [AI Translation Guide](ai-translation.md). #### 5.2.13.3 Translating Content @@ -596,7 +596,7 @@ The AI will translate: - Re-translate if formatting is broken For comprehensive documentation, see: -- [Complete AI Translation Guide](9-ai-translation.md) - Full documentation +- [Complete AI Translation Guide](ai-translation.md) - Full documentation ## 5.3 Statistics @@ -791,7 +791,7 @@ To back up the whole data located on your web server, you can run our simple bac Here you can edit the general, FAQ specific, search, spam protection, spam control center, SEO related, layout settings, Mail setup for SMTP, API settings, online update settings, and if enabled, LDAP configuration of phpMyFAQ. -You can find a detailed description of all settings in the [Configuration key reference](8-configuration.md). +You can find a detailed description of all settings in the [Configuration key reference](configuration.md). ### 5.6.2 FAQ Multi-sites @@ -871,7 +871,80 @@ On this page, phpMyFAQ displays some relevant system information like PHP versio Please use this information when reporting bugs. Additionally, you can check the status of all translation files and see if there are any missing translations. -## 5.7 Using Microsoft Entra ID +## 5.7 Using external identity providers + +phpMyFAQ can integrate with external identity providers for administrator and frontend logins. + +### 5.7.1 Using Keycloak + +Keycloak support uses OpenID Connect Authorization Code flow with PKCE. +You can enable it in the administration under `Configuration` -> `Security` -> `Keycloak`. +For a worked configuration example, see the dedicated [Keycloak Integration guide](keycloak.md). + +Recommended Keycloak client settings: + +- Client type: confidential +- Standard flow enabled +- Direct access grants disabled unless you need them for other tools +- PKCE code challenge method: `S256` +- Root URL: `https://faq.example.com/` +- Home URL: `https://faq.example.com/` +- Valid redirect URI: `https://faq.example.com/auth/keycloak/callback` +- Valid post logout redirect URI: `https://faq.example.com/` +- Web origin: `https://faq.example.com` + +Minimum phpMyFAQ configuration: + +1. Enable `Keycloak sign-in` +2. Set the `Keycloak base URL`, for example `https://sso.example.com` +3. Set the `Realm` +4. Set the `Client ID` +5. Set the `Client secret` +6. Set the `Redirect URI` to your phpMyFAQ callback URL +7. Keep `Scopes` at least on `openid profile email` +8. Optionally set `Logout redirect URL` to the page users should see after provider logout + +Example phpMyFAQ values for a production setup: + +- `keycloak.baseUrl`: `https://sso.example.com` +- `keycloak.realm`: `faq` +- `keycloak.clientId`: `phpmyfaq` +- `keycloak.redirectUri`: `https://faq.example.com/auth/keycloak/callback` +- `keycloak.scopes`: `openid profile email` +- `keycloak.logoutRedirectUrl`: `https://faq.example.com/` + +Optional settings: + +- Enable automatic provisioning if phpMyFAQ should create local users on first successful Keycloak login +- Enable automatic group assignment if phpMyFAQ should assign local groups from Keycloak roles +- Enable group synchronization on login if phpMyFAQ should remove stale memberships for mapped Keycloak groups +- Add a JSON role-to-group mapping if Keycloak role names should map to different phpMyFAQ group names +- Set a logout redirect URL if users should return to a specific page after provider logout +- Use a JSON mapping such as `{"admin":"Administrators","faq-editors":"Editors"}` if Keycloak role names and phpMyFAQ group names differ + +phpMyFAQ resolves users in this order: + +1. existing user linked by stored Keycloak subject (`sub`) +2. preferred username from Keycloak +3. existing user by email address +4. automatic provisioning if enabled + +If automatic provisioning is disabled, users must already exist in phpMyFAQ before they can sign in with Keycloak. + +Group assignment behavior: + +- only roles listed in the JSON mapping are managed by phpMyFAQ +- matched groups are added to the user on login +- if group synchronization on login is enabled, stale memberships for mapped groups are removed during login +- phpMyFAQ groups outside the configured Keycloak mapping are left untouched + +Troubleshooting: + +- If login works but logout does not return to phpMyFAQ, verify `Valid post logout redirect URI` in Keycloak and `keycloak.logoutRedirectUrl` in phpMyFAQ +- If users are created but not added to groups, make sure permission level `medium` is enabled and the Keycloak roles actually match your JSON mapping keys +- If an existing user cannot log in, check whether the stored Keycloak subject (`sub`) is already linked to a different account + +### 5.7.2 Using Microsoft Entra ID You can use our experimental Microsoft Entra ID support for user authentication as well. App Registrations in Azure are used to integrate applications with Microsoft Azure services, diff --git a/docs/configuration.md b/docs/configuration.md index 13342ae485..0daad4e43f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -124,6 +124,18 @@ each setting controls. These values are set during installation and can be chang | `security.enableRegistration` | Enable registration for visitors | `true` | Allows new users to register an account on the FAQ. | | `security.domainWhiteListForRegistrations` | Allowed hosts for registrations | *(empty)* | A list of allowed email domains for new registrations. Leave empty to allow all domains. | | `security.enableSignInWithMicrosoft` | Enable Sign in with Microsoft Entra ID | `false` | Enables authentication via Microsoft Entra ID (formerly Azure AD). | +| `keycloak.enable` | Enable Keycloak sign-in | `false` | Enables OpenID Connect authentication via Keycloak for the frontend and admin login forms. | +| `keycloak.baseUrl` | Keycloak base URL | *(empty)* | Base URL of the Keycloak server, for example `https://sso.example.com`. | +| `keycloak.realm` | Realm | *(empty)* | Keycloak realm used for phpMyFAQ authentication. | +| `keycloak.clientId` | Client ID | *(empty)* | OIDC client identifier configured in Keycloak. | +| `keycloak.clientSecret` | Client secret | *(empty)* | Client secret configured for the Keycloak OIDC client. | +| `keycloak.redirectUri` | Redirect URI | *(empty)* | Callback URL registered in the Keycloak client, usually `https://faq.example.com/auth/keycloak/callback`. | +| `keycloak.scopes` | Scopes | `openid profile email` | Space-separated scopes requested during login. | +| `keycloak.autoProvision` | Automatically create phpMyFAQ users on first Keycloak login | `false` | When enabled, phpMyFAQ creates a local user automatically if no matching account exists yet. | +| `keycloak.groupAutoAssign` | Automatically assign phpMyFAQ groups from Keycloak roles | `false` | When enabled and permission level `medium` is active, phpMyFAQ assigns users to groups derived from Keycloak roles on login. | +| `keycloak.groupSyncOnLogin` | Synchronize mapped phpMyFAQ groups on login | `false` | When enabled, phpMyFAQ also removes stale memberships for groups managed by the Keycloak role mapping during login. | +| `keycloak.groupMapping` | Role to group mapping | *(empty)* | JSON object mapping Keycloak role names to phpMyFAQ group names, for example `{"admin":"Administrators","faq-editors":"Editors"}`. Only mapped roles are managed for assignment and synchronization. | +| `keycloak.logoutRedirectUrl` | Logout redirect URL | *(empty)* | URL users should be redirected to after logging out from Keycloak, for example `https://faq.example.com/`. | | `security.enableGoogleReCaptchaV2` | Enable Invisible Google ReCAPTCHA v2 | `false` | Enables Google reCAPTCHA v2 to protect forms from spam and abuse. | | `security.googleReCaptchaV2SiteKey` | Google ReCAPTCHA v2 site key | *(empty)* | The site key from your Google reCAPTCHA v2 registration. | | `security.googleReCaptchaV2SecretKey` | Google ReCAPTCHA v2 secret key | *(empty)* | The secret key from your Google reCAPTCHA v2 registration. | diff --git a/docs/index.md b/docs/index.md index a89430c8eb..3f1140506d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ phpMyFAQ also supports two-factor authentication (2FA) for enhanced security. An AI-assisted translation feature helps translate content into multiple languages using Google Cloud Translation, DeepL, Azure Translator, Amazon Translate, or LibreTranslate. A REST API is available for integration with other systems. -It also supports OpenLDAP, Microsoft Active Directory, Microsoft Entra ID, and an MCP Server for AI agents. +It also supports OpenLDAP, Microsoft Active Directory, Microsoft Entra ID, Keycloak, and an MCP Server for AI agents. The system is easy to install, thanks to its user-friendly installation script. phpMyFAQ is versatile diff --git a/docs/keycloak.md b/docs/keycloak.md new file mode 100644 index 0000000000..e3bec0783d --- /dev/null +++ b/docs/keycloak.md @@ -0,0 +1,127 @@ +# Keycloak Integration + +phpMyFAQ supports Keycloak as an OpenID Connect provider for frontend and administration logins. +The integration uses Authorization Code flow with PKCE. + +## 1. Prerequisites + +Before you configure phpMyFAQ, make sure you have: + +- a reachable Keycloak server, for example `https://sso.example.com` +- a realm for phpMyFAQ users, for example `faq` +- a confidential client for phpMyFAQ +- the public URL of your phpMyFAQ installation, for example `https://faq.example.com/` + +## 2. Recommended Keycloak client settings + +Recommended client settings for a phpMyFAQ installation at `https://faq.example.com/`: + +- Client type: confidential +- Standard flow enabled +- Direct access grants are disabled unless required for another integration +- Service accounts disabled +- PKCE code challenge method: `S256` +- Root URL: `https://faq.example.com/` +- Home URL: `https://faq.example.com/` +- Valid redirect URIs: + - `https://faq.example.com/auth/keycloak/callback` +- Valid post logout redirect URIs: + - `https://faq.example.com/` +- Web origins: + - `https://faq.example.com` + +## 3. phpMyFAQ configuration + +In phpMyFAQ, open: + +- `Configuration` +- `Security` +- `Keycloak` + +Typical values: + +- `keycloak.enable`: `true` +- `keycloak.baseUrl`: `https://sso.example.com` +- `keycloak.realm`: `faq` +- `keycloak.clientId`: `phpmyfaq` +- `keycloak.clientSecret`: `` +- `keycloak.redirectUri`: `https://faq.example.com/auth/keycloak/callback` +- `keycloak.scopes`: `openid profile email` +- `keycloak.logoutRedirectUrl`: `https://faq.example.com/` + +Optional user and group settings: + +- `keycloak.autoProvision`: `true` +- `keycloak.groupAutoAssign`: `true` +- `keycloak.groupSyncOnLogin`: `true` +- `keycloak.groupMapping`: `{"admin":"Administrators","faq-editors":"Editors"}` + +## 4. User resolution and account linking + +phpMyFAQ resolves Keycloak users in this order: + +1. existing user linked by stored Keycloak subject (`sub`) +2. preferred username from Keycloak +3. existing user by email address +4. automatic provisioning, if enabled + +The stored Keycloak subject is the durable link between a local phpMyFAQ account and the external identity. + +If automatic provisioning is disabled, users must already exist in phpMyFAQ before they can sign in. + +## 5. Group mapping behavior + +Group handling is intentionally conservative: + +- only roles listed in `keycloak.groupMapping` are managed by phpMyFAQ +- mapped groups are added on login when `keycloak.groupAutoAssign` is enabled +- stale memberships are removed only for mapped groups when `keycloak.groupSyncOnLogin` is enabled +- phpMyFAQ groups outside the configured mapping are left untouched + +Example mapping: + +```json +{ + "admin": "Administrators", + "faq-editors": "Editors" +} +``` + +This means: + +- Keycloak role `admin` maps to phpMyFAQ group `Administrators` +- Keycloak role `faq-editors` maps to phpMyFAQ group `Editors` + +## 6. Logout behavior + +phpMyFAQ logs the user out locally and then redirects to Keycloak logout when: + +- Keycloak sign-in is enabled +- the current user is authenticated through Keycloak + +For a reliable provider logout: + +- set `keycloak.logoutRedirectUrl` in phpMyFAQ +- make sure the same URL is listed as a valid post-logout redirect URI in Keycloak + +## 7. Troubleshooting + +If login works but logout does not return to phpMyFAQ: + +- verify `keycloak.logoutRedirectUrl` +- verify the matching valid post-logout redirect URI in Keycloak + +If users are created but not assigned to groups: + +- verify permission level `medium` +- verify `keycloak.groupAutoAssign` is enabled +- verify the Keycloak role names exactly match the JSON mapping keys + +If group synchronization removes the wrong memberships: + +- check `keycloak.groupMapping` +- remember that only mapped groups are managed + +If an existing user cannot log in: + +- check whether the stored Keycloak subject (`sub`) is already linked to another local account diff --git a/eslint.config.mjs b/eslint.config.mjs index 76410a133b..0b869280bc 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,6 +13,7 @@ const ignoresConfig = globalIgnores([ 'phpmyfaq/assets/public/*', 'phpmyfaq/content/upgrades/*', 'phpmyfaq/src/libs/*', + 'site/*', 'volumes/*', ]); diff --git a/mkdocs.yml b/mkdocs.yml index 89576bee33..3f5bff899a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,9 @@ site_name: phpMyFAQ v4.2 theme: name: readthedocs highlightjs: true +not_in_nav: | + /s3-storage-checklist.md + /keys/** extra_css: - css/custom.css plugins: @@ -15,9 +18,10 @@ nav: - 6. Use phpMyFAQ: 'usage.md' - 7. Manage phpMyFAQ: 'administration.md' - 8. Configuration reference: 'configuration.md' - - 9. AI-Assisted Translation feature: 'ai-translation.md' - - 10. Developer documentation: 'development.md' - - 11. Plugins: 'plugins.md' - - 12. MCP Server: 'mcp-server.md' - - 13. Release: 'release.md' - - 14. Thank you!: 'thank-you.md' + - 9. Keycloak Integration: 'keycloak.md' + - 10. AI-Assisted Translation feature: 'ai-translation.md' + - 11. Developer documentation: 'development.md' + - 12. Plugins: 'plugins.md' + - 13. MCP Server: 'mcp-server.md' + - 14. Release: 'release.md' + - 15. Thank you!: 'thank-you.md' diff --git a/package.json b/package.json index e896117cb6..6adb64dc95 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "release:artifacts": "./scripts/prepare-release-artifacts.sh", "release:sign": "./scripts/sign-release-artifacts.sh", "sbom": "./scripts/generate-sbom.sh", + "docs:build": "mkdocs build --strict", "eslint": "eslint .", "lint": "prettier --check .", "lint:fix": "prettier --write .", diff --git a/phpmyfaq/admin/assets/src/configuration/configuration.test.ts b/phpmyfaq/admin/assets/src/configuration/configuration.test.ts index 0e2be2814e..cf0d8088a2 100644 --- a/phpmyfaq/admin/assets/src/configuration/configuration.test.ts +++ b/phpmyfaq/admin/assets/src/configuration/configuration.test.ts @@ -294,12 +294,16 @@ describe('Configuration Functions', () => { +
+
`; (fetchConfiguration as Mock).mockResolvedValue('Configuration content'); @@ -323,6 +327,13 @@ describe('Configuration Functions', () => { +
  • Integrations
  • + +
  • Maintenance
  • +