mirror of
https://github.com/error311/FileRise.git
synced 2026-05-12 23:10:05 -05:00
release(v3.2.4): OIDC group-claim mapping + extra scopes (Authentik & Keycloak-friendly) + sponsor list update
- OIDC: add configurable group claim + extra scopes (Admin + env overrides) - OIDC: extract group tags from both userinfo and ID token, supports dot-path claims (e.g. realm_access.roles) - Admin: surface effective & locked groupClaim + extraScopes values and include them in OIDC debug snapshot - Docs OpenAPI: document new OIDC config fields - Admin: add new Pro supporter name to thanks list
This commit is contained in:
@@ -1,5 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## Changes 01/30/2026 (v3.2.4)
|
||||
|
||||
`release(v3.2.4): OIDC group-claim mapping + extra scopes (Authentik & Keycloak-friendly) + sponsor list update`
|
||||
|
||||
**Commit message**
|
||||
|
||||
```text
|
||||
release(v3.2.4): OIDC group-claim mapping + extra scopes (Authentik & Keycloak-friendly) + sponsor list update
|
||||
|
||||
- OIDC: add configurable group claim + extra scopes (Admin + env overrides)
|
||||
- OIDC: extract group tags from both userinfo and ID token, supports dot-path claims (e.g. realm_access.roles)
|
||||
- Admin: surface effective & locked groupClaim + extraScopes values and include them in OIDC debug snapshot
|
||||
- Docs OpenAPI: document new OIDC config fields
|
||||
- Admin: add new Pro supporter name to thanks list
|
||||
```
|
||||
|
||||
**Added**
|
||||
|
||||
- **OIDC: configurable group claim name**
|
||||
- Admin setting: `oidc.groupClaim` (default behavior remains `groups`)
|
||||
- Env override: `FR_OIDC_GROUP_CLAIM` (locks Admin field when set)
|
||||
- Supports **dot-path claims** (example: `realm_access.roles`)
|
||||
- **OIDC: extra scopes**
|
||||
- Admin setting: `oidc.extraScopes` (space/comma separated)
|
||||
- Env override: `FR_OIDC_EXTRA_SCOPES` (locks Admin field when set)
|
||||
- Effective scopes become: `openid profile email` + your extras
|
||||
- **OIDC debug snapshot improvements**
|
||||
- `/api/admin/oidcDebugInfo.php` now shows:
|
||||
- `groupClaim` + source (`env|config|default`)
|
||||
- `extraScopes` + source (`env|config|none`)
|
||||
- final `scopes[]` list
|
||||
|
||||
**Changed**
|
||||
|
||||
- **Group mapping reads both claim sets**
|
||||
- Group tags are extracted from:
|
||||
- Userinfo response, and
|
||||
- ID Token payload (when available from the OIDC library)
|
||||
- This improves compatibility with IdPs that only place groups/roles in one of those.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Group mapping reliability with IdPs like Authentik/Keycloak where:
|
||||
- groups are not under the default `groups` claim, and/or
|
||||
- groups require requesting an additional scope.
|
||||
|
||||
**Security / Hardening**
|
||||
|
||||
- `groupClaim` and `extraScopes` inputs are sanitized on save (control chars stripped + length capped).
|
||||
- No user-controlled HTML is introduced; config values are escaped in the Admin UI.
|
||||
- No secrets are logged or echoed back.
|
||||
|
||||
---
|
||||
|
||||
## Changes 01/29/2026 (v3.2.3)
|
||||
|
||||
`release(v3.2.3): resumable upload UX fixes + stale chunk cleanup + folder re-upload conflict handling (closes #100, closes #101, closes #102)`
|
||||
|
||||
@@ -83,6 +83,12 @@ if (!defined('FR_OIDC_GROUP_CLAIM')) {
|
||||
);
|
||||
}
|
||||
|
||||
// Optional extra OIDC scopes to request (space/comma separated).
|
||||
if (!defined('FR_OIDC_EXTRA_SCOPES')) {
|
||||
$envVal = getenv('FR_OIDC_EXTRA_SCOPES');
|
||||
define('FR_OIDC_EXTRA_SCOPES', ($envVal !== false) ? trim((string)$envVal) : '');
|
||||
}
|
||||
|
||||
// Name of an IdP group that should be treated as "FileRise admin".
|
||||
if (!defined('FR_OIDC_ADMIN_GROUP')) {
|
||||
$envVal = getenv('FR_OIDC_ADMIN_GROUP');
|
||||
|
||||
@@ -69,6 +69,50 @@ try {
|
||||
|
||||
$loginOptions = is_array($cfg['loginOptions'] ?? null) ? $cfg['loginOptions'] : [];
|
||||
|
||||
$envGroupClaim = getenv('FR_OIDC_GROUP_CLAIM');
|
||||
$cfgGroupClaim = isset($oidcCfg['groupClaim']) ? trim((string)$oidcCfg['groupClaim']) : '';
|
||||
$groupClaimSource = 'default';
|
||||
$groupClaimEffective = (defined('FR_OIDC_GROUP_CLAIM') && trim((string)FR_OIDC_GROUP_CLAIM) !== '')
|
||||
? trim((string)FR_OIDC_GROUP_CLAIM)
|
||||
: 'groups';
|
||||
if ($envGroupClaim !== false && trim((string)$envGroupClaim) !== '') {
|
||||
$groupClaimEffective = trim((string)$envGroupClaim);
|
||||
$groupClaimSource = 'env';
|
||||
} elseif ($cfgGroupClaim !== '') {
|
||||
$groupClaimEffective = $cfgGroupClaim;
|
||||
$groupClaimSource = 'config';
|
||||
}
|
||||
|
||||
$envExtraScopes = getenv('FR_OIDC_EXTRA_SCOPES');
|
||||
$cfgExtraScopes = isset($oidcCfg['extraScopes']) ? trim((string)$oidcCfg['extraScopes']) : '';
|
||||
$extraScopesSource = 'none';
|
||||
$extraScopesEffective = '';
|
||||
if ($envExtraScopes !== false && trim((string)$envExtraScopes) !== '') {
|
||||
$extraScopesEffective = trim((string)$envExtraScopes);
|
||||
$extraScopesSource = 'env';
|
||||
} elseif ($cfgExtraScopes !== '') {
|
||||
$extraScopesEffective = $cfgExtraScopes;
|
||||
$extraScopesSource = 'config';
|
||||
}
|
||||
|
||||
$scopes = ['openid', 'profile', 'email'];
|
||||
if ($extraScopesEffective !== '') {
|
||||
foreach (preg_split('/[,\s]+/', $extraScopesEffective) ?: [] as $scope) {
|
||||
$scope = trim($scope);
|
||||
if ($scope !== '') {
|
||||
$scopes[] = $scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
$scopeSet = [];
|
||||
foreach ($scopes as $scope) {
|
||||
$key = strtolower($scope);
|
||||
if (!isset($scopeSet[$key])) {
|
||||
$scopeSet[$key] = $scope;
|
||||
}
|
||||
}
|
||||
$scopes = array_values($scopeSet);
|
||||
|
||||
$info = [
|
||||
'providerUrl' => $oidcCfg['providerUrl'] ?? ($cfg['oidc_provider_url'] ?? null),
|
||||
'redirectUri' => $oidcCfg['redirectUri'] ?? ($cfg['oidc_redirect_uri'] ?? null),
|
||||
@@ -86,7 +130,11 @@ try {
|
||||
],
|
||||
|
||||
'tokenEndpointAuthMethod' => $tokenAuthMethod ?: '(library default)',
|
||||
'scopes' => ['openid', 'profile', 'email'],
|
||||
'groupClaim' => $groupClaimEffective,
|
||||
'groupClaimSource' => $groupClaimSource,
|
||||
'extraScopes' => $extraScopesEffective,
|
||||
'extraScopesSource' => $extraScopesSource,
|
||||
'scopes' => $scopes,
|
||||
|
||||
'loginOptions' => [
|
||||
'disableFormLogin' => !empty($loginOptions['disableFormLogin']),
|
||||
|
||||
@@ -2589,6 +2589,8 @@ function captureInitialAdminConfig() {
|
||||
oidcDebugLogging: !!document.getElementById("oidcDebugLogging")?.checked,
|
||||
oidcRedirectUri: (document.getElementById("oidcRedirectUri")?.value || "").trim(),
|
||||
oidcAllowDemote: !!document.getElementById("oidcAllowDemote")?.checked,
|
||||
oidcGroupClaim: (document.getElementById("oidcGroupClaim")?.value || "").trim(),
|
||||
oidcExtraScopes: (document.getElementById("oidcExtraScopes")?.value || "").trim(),
|
||||
|
||||
// UI is now “enable” toggles
|
||||
enableFormLogin: !!document.getElementById("enableFormLogin")?.checked,
|
||||
@@ -2651,6 +2653,8 @@ function hasUnsavedChanges() {
|
||||
getVal("oidcRedirectUri") !== o.oidcRedirectUri ||
|
||||
getChk("oidcAllowDemote") !== o.oidcAllowDemote ||
|
||||
getChk("oidcDebugLogging") !== o.oidcDebugLogging ||
|
||||
getVal("oidcGroupClaim") !== (o.oidcGroupClaim || "") ||
|
||||
getVal("oidcExtraScopes") !== (o.oidcExtraScopes || "") ||
|
||||
|
||||
// new enable-toggles
|
||||
getChk("enableFormLogin") !== o.enableFormLogin ||
|
||||
@@ -5430,6 +5434,40 @@ export function openAdminPanel() {
|
||||
const oidcDebugEnabled = !!(config.oidc && config.oidc.debugLogging);
|
||||
const oidcAllowDemote = !!(config.oidc && config.oidc.allowDemote);
|
||||
const oidcPublicClient = !!(config.oidc && config.oidc.publicClient);
|
||||
const oidcGroupClaimCfg = (config.oidc && typeof config.oidc.groupClaim === 'string')
|
||||
? config.oidc.groupClaim
|
||||
: '';
|
||||
const oidcGroupClaimEffective = (config.oidc && typeof config.oidc.groupClaimEffective === 'string')
|
||||
? config.oidc.groupClaimEffective
|
||||
: '';
|
||||
const oidcGroupClaimLocked = !!(config.oidc && config.oidc.groupClaimLockedByEnv);
|
||||
const oidcGroupClaimValue = oidcGroupClaimLocked ? oidcGroupClaimEffective : oidcGroupClaimCfg;
|
||||
const oidcExtraScopesCfg = (config.oidc && typeof config.oidc.extraScopes === 'string')
|
||||
? config.oidc.extraScopes
|
||||
: '';
|
||||
const oidcExtraScopesEffective = (config.oidc && typeof config.oidc.extraScopesEffective === 'string')
|
||||
? config.oidc.extraScopesEffective
|
||||
: '';
|
||||
const oidcExtraScopesLocked = !!(config.oidc && config.oidc.extraScopesLockedByEnv);
|
||||
const oidcExtraScopesValue = oidcExtraScopesLocked ? oidcExtraScopesEffective : oidcExtraScopesCfg;
|
||||
const oidcGroupClaimHelpDefault = tf(
|
||||
"oidc_group_claim_help",
|
||||
"Claim name for IdP groups (supports dot paths like realm_access.roles). Leave blank to use \"groups\"."
|
||||
);
|
||||
const oidcGroupClaimHelpLocked = tf(
|
||||
"oidc_group_claim_locked",
|
||||
"Locked by FR_OIDC_GROUP_CLAIM env override."
|
||||
);
|
||||
const oidcGroupClaimHelp = oidcGroupClaimLocked ? oidcGroupClaimHelpLocked : oidcGroupClaimHelpDefault;
|
||||
const oidcExtraScopesHelpDefault = tf(
|
||||
"oidc_extra_scopes_help",
|
||||
"Space/comma-separated scopes to request in addition to openid/profile/email (example: groups)."
|
||||
);
|
||||
const oidcExtraScopesHelpLocked = tf(
|
||||
"oidc_extra_scopes_locked",
|
||||
"Locked by FR_OIDC_EXTRA_SCOPES env override."
|
||||
);
|
||||
const oidcExtraScopesHelp = oidcExtraScopesLocked ? oidcExtraScopesHelpLocked : oidcExtraScopesHelpDefault;
|
||||
|
||||
const oidcHtml = `
|
||||
<hr class="admin-divider">
|
||||
@@ -5497,6 +5535,24 @@ export function openAdminPanel() {
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcGroupClaim">${tf("oidc_group_claim_label", "Group claim name")}:</label>
|
||||
<input type="text" id="oidcGroupClaim" class="form-control"
|
||||
placeholder="groups"
|
||||
value="${escapeHTML(oidcGroupClaimValue || "")}"
|
||||
${oidcGroupClaimLocked ? "disabled data-locked='1'" : ""} />
|
||||
<small class="text-muted">${escapeHTML(oidcGroupClaimHelp)}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="oidcExtraScopes">${tf("oidc_extra_scopes_label", "Extra OIDC scopes")}:</label>
|
||||
<input type="text" id="oidcExtraScopes" class="form-control"
|
||||
placeholder="groups"
|
||||
value="${escapeHTML(oidcExtraScopesValue || "")}"
|
||||
${oidcExtraScopesLocked ? "disabled data-locked='1'" : ""} />
|
||||
<small class="text-muted">${escapeHTML(oidcExtraScopesHelp)}</small>
|
||||
</div>
|
||||
|
||||
<hr class="admin-divider">
|
||||
|
||||
<div class="form-group" style="margin-top:4px;">
|
||||
@@ -6792,6 +6848,36 @@ ${t("shared_max_upload_size_bytes")}
|
||||
if (ooCont) wireReplaceButtons(ooCont);
|
||||
document.getElementById("oidcClientSecret").value = window.currentOIDCConfig?.clientSecret || "";
|
||||
document.getElementById("oidcRedirectUri").value = window.currentOIDCConfig?.redirectUri || "";
|
||||
const oidcGroupClaimEl = document.getElementById("oidcGroupClaim");
|
||||
if (oidcGroupClaimEl) {
|
||||
const locked = !!(config.oidc && config.oidc.groupClaimLockedByEnv);
|
||||
const value = locked
|
||||
? (config.oidc.groupClaimEffective || "")
|
||||
: (config.oidc.groupClaim || "");
|
||||
oidcGroupClaimEl.value = value;
|
||||
if (locked) {
|
||||
oidcGroupClaimEl.disabled = true;
|
||||
oidcGroupClaimEl.dataset.locked = "1";
|
||||
} else {
|
||||
oidcGroupClaimEl.disabled = false;
|
||||
oidcGroupClaimEl.removeAttribute("data-locked");
|
||||
}
|
||||
}
|
||||
const oidcExtraScopesEl = document.getElementById("oidcExtraScopes");
|
||||
if (oidcExtraScopesEl) {
|
||||
const locked = !!(config.oidc && config.oidc.extraScopesLockedByEnv);
|
||||
const value = locked
|
||||
? (config.oidc.extraScopesEffective || "")
|
||||
: (config.oidc.extraScopes || "");
|
||||
oidcExtraScopesEl.value = value;
|
||||
if (locked) {
|
||||
oidcExtraScopesEl.disabled = true;
|
||||
oidcExtraScopesEl.dataset.locked = "1";
|
||||
} else {
|
||||
oidcExtraScopesEl.disabled = false;
|
||||
oidcExtraScopesEl.removeAttribute("data-locked");
|
||||
}
|
||||
}
|
||||
document.getElementById("globalOtpauthUrl").value = window.currentOIDCConfig?.globalOtpauthUrl || '';
|
||||
const oidcDebugEl = document.getElementById('oidcDebugLogging');
|
||||
if (oidcDebugEl) {
|
||||
@@ -6915,6 +7001,8 @@ function handleSave() {
|
||||
debugLogging: !!document.getElementById("oidcDebugLogging")?.checked,
|
||||
allowDemote: !!document.getElementById("oidcAllowDemote")?.checked,
|
||||
publicClient: oidcPublicClient,
|
||||
groupClaim: (document.getElementById("oidcGroupClaim")?.value || "").trim(),
|
||||
extraScopes: (document.getElementById("oidcExtraScopes")?.value || "").trim(),
|
||||
// clientId/clientSecret added conditionally below
|
||||
},
|
||||
globalOtpauthUrl: document
|
||||
|
||||
@@ -34,7 +34,8 @@ const DEFAULT_SUPPORTERS = [
|
||||
'Edisto Pirates of SC',
|
||||
'@jubnl',
|
||||
'Juozaitis S',
|
||||
'Cocoseb31'
|
||||
'Cocoseb31',
|
||||
'nulltrope'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -309,6 +309,16 @@ class AdminController
|
||||
$ignoreCfg = (string)($config['ignoreRegex'] ?? '');
|
||||
$ignoreEffective = $ignoreLockedByEnv ? trim((string)$envIgnore) : $ignoreCfg;
|
||||
|
||||
$envGroupClaim = getenv('FR_OIDC_GROUP_CLAIM');
|
||||
$groupClaimLockedByEnv = ($envGroupClaim !== false && trim((string)$envGroupClaim) !== '');
|
||||
$groupClaimCfg = (string)($config['oidc']['groupClaim'] ?? '');
|
||||
$groupClaimEffective = $groupClaimLockedByEnv ? trim((string)$envGroupClaim) : $groupClaimCfg;
|
||||
|
||||
$envExtraScopes = getenv('FR_OIDC_EXTRA_SCOPES');
|
||||
$extraScopesLockedByEnv = ($envExtraScopes !== false && trim((string)$envExtraScopes) !== '');
|
||||
$extraScopesCfg = (string)($config['oidc']['extraScopes'] ?? '');
|
||||
$extraScopesEffective = $extraScopesLockedByEnv ? trim((string)$envExtraScopes) : $extraScopesCfg;
|
||||
|
||||
$adminExtra = [
|
||||
'loginOptions' => array_merge($public['loginOptions'], [
|
||||
'authBypass' => (bool)($config['loginOptions']['authBypass'] ?? false),
|
||||
@@ -320,6 +330,12 @@ class AdminController
|
||||
'debugLogging' => !empty($config['oidc']['debugLogging']),
|
||||
'allowDemote' => !empty($config['oidc']['allowDemote']),
|
||||
'publicClient' => !empty($config['oidc']['publicClient']),
|
||||
'groupClaim' => $groupClaimCfg,
|
||||
'groupClaimEffective' => $groupClaimEffective,
|
||||
'groupClaimLockedByEnv' => $groupClaimLockedByEnv,
|
||||
'extraScopes' => $extraScopesCfg,
|
||||
'extraScopesEffective' => $extraScopesEffective,
|
||||
'extraScopesLockedByEnv' => $extraScopesLockedByEnv,
|
||||
]),
|
||||
'onlyoffice' => [
|
||||
'enabled' => $effEnabled,
|
||||
@@ -2417,6 +2433,8 @@ class AdminController
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'redirectUri' => '',
|
||||
'groupClaim' => '',
|
||||
'extraScopes' => '',
|
||||
'publicClient' => false,
|
||||
],
|
||||
'branding' => [
|
||||
@@ -2569,6 +2587,29 @@ class AdminController
|
||||
}
|
||||
}
|
||||
|
||||
// OIDC group claim + extra scopes (env overrides lock these fields)
|
||||
$envGroupClaim = getenv('FR_OIDC_GROUP_CLAIM');
|
||||
$groupClaimLockedByEnv = ($envGroupClaim !== false && trim((string)$envGroupClaim) !== '');
|
||||
if (!$groupClaimLockedByEnv && array_key_exists('groupClaim', $data['oidc'] ?? [])) {
|
||||
$raw = trim((string)$data['oidc']['groupClaim']);
|
||||
$raw = preg_replace('/[\x00-\x1F\x7F]/', '', $raw);
|
||||
if (strlen($raw) > 200) {
|
||||
$raw = substr($raw, 0, 200);
|
||||
}
|
||||
$merged['oidc']['groupClaim'] = $raw;
|
||||
}
|
||||
|
||||
$envExtraScopes = getenv('FR_OIDC_EXTRA_SCOPES');
|
||||
$extraScopesLockedByEnv = ($envExtraScopes !== false && trim((string)$envExtraScopes) !== '');
|
||||
if (!$extraScopesLockedByEnv && array_key_exists('extraScopes', $data['oidc'] ?? [])) {
|
||||
$raw = trim((string)$data['oidc']['extraScopes']);
|
||||
$raw = preg_replace('/[\x00-\x1F\x7F]/', '', $raw);
|
||||
if (strlen($raw) > 500) {
|
||||
$raw = substr($raw, 0, 500);
|
||||
}
|
||||
$merged['oidc']['extraScopes'] = $raw;
|
||||
}
|
||||
|
||||
// oidc: only overwrite non-empty inputs; validate when enabling OIDC
|
||||
foreach (['providerUrl', 'clientId', 'clientSecret', 'redirectUri'] as $f) {
|
||||
if (!empty($data['oidc'][$f])) {
|
||||
|
||||
@@ -114,6 +114,61 @@ class AuthController
|
||||
return $oidcAction ?: null;
|
||||
}
|
||||
|
||||
protected function resolveOidcGroupClaim(array $cfg = []): string
|
||||
{
|
||||
$envVal = getenv('FR_OIDC_GROUP_CLAIM');
|
||||
if ($envVal !== false && trim((string)$envVal) !== '') {
|
||||
return trim((string)$envVal);
|
||||
}
|
||||
|
||||
$oidcCfg = is_array($cfg['oidc'] ?? null) ? $cfg['oidc'] : [];
|
||||
$cfgVal = isset($oidcCfg['groupClaim']) ? trim((string)$oidcCfg['groupClaim']) : '';
|
||||
if ($cfgVal !== '') {
|
||||
return $cfgVal;
|
||||
}
|
||||
|
||||
if (defined('FR_OIDC_GROUP_CLAIM') && trim((string)FR_OIDC_GROUP_CLAIM) !== '') {
|
||||
return (string)FR_OIDC_GROUP_CLAIM;
|
||||
}
|
||||
|
||||
return 'groups';
|
||||
}
|
||||
|
||||
protected function resolveOidcExtraScopes(array $cfg = []): string
|
||||
{
|
||||
$envVal = getenv('FR_OIDC_EXTRA_SCOPES');
|
||||
if ($envVal !== false && trim((string)$envVal) !== '') {
|
||||
return trim((string)$envVal);
|
||||
}
|
||||
|
||||
$oidcCfg = is_array($cfg['oidc'] ?? null) ? $cfg['oidc'] : [];
|
||||
return isset($oidcCfg['extraScopes']) ? trim((string)$oidcCfg['extraScopes']) : '';
|
||||
}
|
||||
|
||||
protected function getOidcScopes(array $cfg = []): array
|
||||
{
|
||||
$scopes = ['openid', 'profile', 'email'];
|
||||
$extra = $this->resolveOidcExtraScopes($cfg);
|
||||
if ($extra !== '') {
|
||||
foreach (preg_split('/[,\s]+/', $extra) ?: [] as $scope) {
|
||||
$scope = trim($scope);
|
||||
if ($scope !== '') {
|
||||
$scopes[] = $scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unique = [];
|
||||
foreach ($scopes as $scope) {
|
||||
$key = strtolower($scope);
|
||||
if (!isset($unique[$key])) {
|
||||
$unique[$key] = $scope;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($unique);
|
||||
}
|
||||
|
||||
protected function handleOidcFlow(): bool
|
||||
{
|
||||
$oidcAction = $this->getOidcAction();
|
||||
@@ -190,7 +245,7 @@ class AuthController
|
||||
}
|
||||
|
||||
$oidc->setRedirectURL($cfg['oidc']['redirectUri']);
|
||||
$oidc->addScope(['openid', 'profile', 'email']);
|
||||
$oidc->addScope($this->getOidcScopes($cfg));
|
||||
|
||||
if ($oidcAction === 'callback') {
|
||||
try {
|
||||
@@ -214,7 +269,13 @@ class AuthController
|
||||
// Pull full userinfo once so we can inspect groups / roles
|
||||
$userinfo = $oidc->requestUserInfo();
|
||||
|
||||
$normalizedTags = $this->extractOidcGroupTags($userinfo);
|
||||
$groupClaim = $this->resolveOidcGroupClaim($cfg);
|
||||
$idTokenClaims = null;
|
||||
if (method_exists($oidc, 'getIdTokenPayload')) {
|
||||
$idTokenClaims = $oidc->getIdTokenPayload();
|
||||
}
|
||||
|
||||
$normalizedTags = $this->extractOidcGroupTags($userinfo, $idTokenClaims, $groupClaim);
|
||||
|
||||
$this->logOidcDebug('OIDC userinfo summary', [
|
||||
'username' => $username,
|
||||
@@ -326,11 +387,31 @@ class AuthController
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function extractOidcGroupTags(object $userinfo): array
|
||||
protected function extractOidcGroupTags($userinfo, $idTokenClaims = null, string $groupClaim = ''): array
|
||||
{
|
||||
// Collect groups/roles from various fields (same logic as before)
|
||||
$rawTags = [];
|
||||
|
||||
$readClaim = function ($root, string $path) {
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
if (!is_object($root) && !is_array($root)) {
|
||||
return null;
|
||||
}
|
||||
$cur = $root;
|
||||
foreach (explode('.', $path) as $seg) {
|
||||
if (is_object($cur) && isset($cur->$seg)) {
|
||||
$cur = $cur->$seg;
|
||||
} elseif (is_array($cur) && array_key_exists($seg, $cur)) {
|
||||
$cur = $cur[$seg];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $cur;
|
||||
};
|
||||
|
||||
$addTags = function ($val) use (&$rawTags) {
|
||||
if (is_array($val)) {
|
||||
foreach ($val as $v) {
|
||||
@@ -351,24 +432,50 @@ class AuthController
|
||||
};
|
||||
|
||||
// 1) Common flat claims (includes "usergroups" which you mentioned)
|
||||
foreach (['groups', 'group', 'usergroups', 'user_groups', 'roles'] as $field) {
|
||||
if (isset($userinfo->$field)) {
|
||||
$addTags($userinfo->$field);
|
||||
$customClaim = trim($groupClaim);
|
||||
if ($customClaim === '' && defined('FR_OIDC_GROUP_CLAIM')) {
|
||||
$customClaim = trim((string)FR_OIDC_GROUP_CLAIM);
|
||||
}
|
||||
$fields = [];
|
||||
if ($customClaim !== '') {
|
||||
$fields[] = $customClaim;
|
||||
}
|
||||
$fields = array_merge($fields, ['groups', 'group', 'usergroups', 'user_groups', 'roles']);
|
||||
$fields = array_values(array_unique($fields));
|
||||
|
||||
$claimSets = [$userinfo];
|
||||
if ($idTokenClaims !== null) {
|
||||
$claimSets[] = $idTokenClaims;
|
||||
}
|
||||
|
||||
foreach ($claimSets as $claims) {
|
||||
foreach ($fields as $field) {
|
||||
$value = $readClaim($claims, $field);
|
||||
if ($value !== null) {
|
||||
$addTags($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) realm_access.roles (Keycloak realm roles)
|
||||
if (isset($userinfo->realm_access) && is_object($userinfo->realm_access)
|
||||
&& isset($userinfo->realm_access->roles) && is_array($userinfo->realm_access->roles)
|
||||
) {
|
||||
$addTags($userinfo->realm_access->roles);
|
||||
}
|
||||
// 2) realm_access.roles (Keycloak realm roles)
|
||||
$realmRoles = $readClaim($claims, 'realm_access.roles');
|
||||
if ($realmRoles !== null) {
|
||||
$addTags($realmRoles);
|
||||
}
|
||||
|
||||
// 3) resource_access.<client>.roles (Keycloak client roles)
|
||||
if (isset($userinfo->resource_access) && is_object($userinfo->resource_access)) {
|
||||
foreach (get_object_vars($userinfo->resource_access) as $clientObj) {
|
||||
if (is_object($clientObj) && isset($clientObj->roles) && is_array($clientObj->roles)) {
|
||||
$addTags($clientObj->roles);
|
||||
// 3) resource_access.<client>.roles (Keycloak client roles)
|
||||
$resourceAccess = null;
|
||||
if (is_object($claims) && isset($claims->resource_access) && is_object($claims->resource_access)) {
|
||||
$resourceAccess = get_object_vars($claims->resource_access);
|
||||
} elseif (is_array($claims) && isset($claims['resource_access']) && is_array($claims['resource_access'])) {
|
||||
$resourceAccess = $claims['resource_access'];
|
||||
}
|
||||
if (is_array($resourceAccess)) {
|
||||
foreach ($resourceAccess as $clientObj) {
|
||||
if (is_object($clientObj) && isset($clientObj->roles)) {
|
||||
$addTags($clientObj->roles);
|
||||
} elseif (is_array($clientObj) && isset($clientObj['roles'])) {
|
||||
$addTags($clientObj['roles']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,12 +830,14 @@ class AdminModel
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'redirectUri' => '',
|
||||
'groupClaim' => '',
|
||||
'extraScopes' => '',
|
||||
'debugLogging' => false,
|
||||
'allowDemote' => false,
|
||||
'publicClient' => false,
|
||||
];
|
||||
} else {
|
||||
foreach (['providerUrl', 'clientId', 'clientSecret', 'redirectUri'] as $k) {
|
||||
foreach (['providerUrl', 'clientId', 'clientSecret', 'redirectUri', 'groupClaim', 'extraScopes'] as $k) {
|
||||
if (!isset($config['oidc'][$k]) || !is_string($config['oidc'][$k])) {
|
||||
$config['oidc'][$k] = '';
|
||||
}
|
||||
@@ -1097,6 +1099,8 @@ class AdminModel
|
||||
'clientId' => '',
|
||||
'clientSecret' => '',
|
||||
'redirectUri' => 'https://yourdomain.com/api/auth/auth.php?oidc=callback',
|
||||
'groupClaim' => '',
|
||||
'extraScopes' => '',
|
||||
'debugLogging' => false,
|
||||
'allowDemote' => false,
|
||||
'publicClient' => false,
|
||||
|
||||
@@ -191,7 +191,9 @@ use OpenApi\Annotations as OA;
|
||||
* @OA\Property(property="providerUrl", type="string", format="uri", example="https://issuer.example.com"),
|
||||
* @OA\Property(property="clientId", type="string", example="my-client-id"),
|
||||
* @OA\Property(property="clientSecret", type="string", writeOnly=true, example="***"),
|
||||
* @OA\Property(property="redirectUri", type="string", format="uri", example="https://app.example.com/auth/callback")
|
||||
* @OA\Property(property="redirectUri", type="string", format="uri", example="https://app.example.com/auth/callback"),
|
||||
* @OA\Property(property="groupClaim", type="string", example="groups", nullable=true),
|
||||
* @OA\Property(property="extraScopes", type="string", example="groups", nullable=true)
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
|
||||
Reference in New Issue
Block a user