From bc1537be0c0cbc48a26034d8b2e24e6bb3dc8181 Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Wed, 9 Sep 2026 11:24:19 +0200 Subject: [PATCH] fix(microsoft): resolve AADSTS50194 by enforcing tenant-specific endpoint planningsagenda.nl instead of common for single-tenant app --- admin.php | 12 ++- auth/callback_microsoft.php | 2 +- auth/oauth_microsoft.php | 12 +++ includes/integrations/microsoft.php | 139 +++++++++++++++++----------- 4 files changed, 103 insertions(+), 62 deletions(-) diff --git a/admin.php b/admin.php index 3e2357a..6935fb9 100644 --- a/admin.php +++ b/admin.php @@ -32,8 +32,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($action === 'save_azure') { $clientId = trim($_POST['client_id'] ?? ''); $clientSecret = trim($_POST['client_secret'] ?? ''); - $tenantId = trim($_POST['tenant_id'] ?? 'common'); - if (empty($tenantId)) $tenantId = 'common'; + $tenantId = trim($_POST['tenant_id'] ?? ''); + if (empty($tenantId) || $tenantId === 'common') { + $tenantId = 'planningsagenda.nl'; + } $isDemo = isset($_POST['is_demo_mode']) ? 1 : 0; $stmtExisting = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'"); @@ -284,10 +286,10 @@ require_once __DIR__ . '/includes/header.php';
- - Gebruik common of uw specifieke domeinnaam (bijv. planningsagenda.nl) + + Vul uw domeinnaam in (planningsagenda.nl) of de Directory (tenant) ID uit Azure. Let op: Gebruik géén 'common' voor single-tenant apps.
diff --git a/auth/callback_microsoft.php b/auth/callback_microsoft.php index 6087e3e..c1f00bd 100644 --- a/auth/callback_microsoft.php +++ b/auth/callback_microsoft.php @@ -77,7 +77,7 @@ $appConfig = microsoft_get_app_config(); $clientId = $appConfig['client_id']; $clientSecret = $appConfig['client_secret']; -$tenantId = $appConfig['tenant_id']; +$tenantId = (!empty($appConfig['tenant_id']) && $appConfig['tenant_id'] !== 'common') ? $appConfig['tenant_id'] : 'planningsagenda.nl'; if (empty($clientId) || empty($clientSecret)) { header('Location: /profile.php?error=' . urlencode('Microsoft 365 configuratie (Client ID of Secret) ontbreekt in Fociz. Neem contact op met de beheerder.')); diff --git a/auth/oauth_microsoft.php b/auth/oauth_microsoft.php index f30a180..2257126 100644 --- a/auth/oauth_microsoft.php +++ b/auth/oauth_microsoft.php @@ -15,6 +15,18 @@ $appConfig = microsoft_get_app_config(); $clientId = !empty($_GET['client_id']) ? trim($_GET['client_id']) : $appConfig['client_id']; $tenantId = !empty($_GET['tenant_id']) ? trim($_GET['tenant_id']) : $appConfig['tenant_id']; +// Voorkom AADSTS50194: single-tenant Azure applicaties mogen NOOIT het /common endpoint gebruiken +if (empty($tenantId) || $tenantId === 'common') { + $userEmail = $user['email'] ?? ''; + $parts = explode('@', $userEmail); + $domain = strtolower(trim(end($parts))); + if (!empty($domain) && !in_array($domain, ['gmail.com', 'outlook.com', 'hotmail.com', 'live.com', 'yahoo.com', 'icloud.com'])) { + $tenantId = $domain; + } else { + $tenantId = 'planningsagenda.nl'; + } +} + if (empty($clientId)) { $err = ($user['role'] === 'ADMIN') ? 'Vul eerst de Application (Client) ID en het Client Secret in bij het Systeembeheer om te koppelen met Microsoft 365.' diff --git a/includes/integrations/microsoft.php b/includes/integrations/microsoft.php index 10bea3d..72d7be8 100644 --- a/includes/integrations/microsoft.php +++ b/includes/integrations/microsoft.php @@ -24,74 +24,100 @@ function microsoft_get_app_config(): array { $clientSecret = fociz_env('MICROSOFT_CLIENT_SECRET', ''); $tenantId = fociz_env('MICROSOFT_TENANT_ID', ''); - if (empty($clientId) || empty($clientSecret)) { - try { - $db = get_db_connection(); - // Zoek bij actieve beheerder(s) in de integrations tabel - $stmt = $db->query(" - SELECT i.settings - FROM integrations i - JOIN users u ON i.user_id = u.id - WHERE u.role = 'ADMIN' AND i.provider = 'OUTLOOK' AND i.settings IS NOT NULL - ORDER BY i.updated_at DESC - LIMIT 5 + try { + $db = get_db_connection(); + $adminOrgDomain = ''; + + // Zoek bij actieve beheerder(s) in de integrations tabel + $stmt = $db->query(" + SELECT i.settings, u.email as admin_email + FROM integrations i + JOIN users u ON i.user_id = u.id + WHERE u.role = 'ADMIN' AND i.provider = 'OUTLOOK' AND i.settings IS NOT NULL + ORDER BY i.updated_at DESC + LIMIT 5 + "); + $rows = $stmt->fetchAll(); + foreach ($rows as $row) { + if (!empty($row['admin_email']) && empty($adminOrgDomain)) { + $emailParts = explode('@', $row['admin_email']); + $domain = strtolower(trim(end($emailParts))); + if (!in_array($domain, ['gmail.com', 'outlook.com', 'hotmail.com', 'live.com', 'yahoo.com', 'icloud.com'])) { + $adminOrgDomain = $domain; + } + } + if (!empty($row['settings'])) { + $saved = json_decode($row['settings'], true) ?: []; + if (empty($clientId) && !empty($saved['client_id'])) { + $clientId = trim($saved['client_id']); + } + if (empty($clientSecret) && !empty($saved['client_secret'])) { + $clientSecret = trim($saved['client_secret']); + } + if ((empty($tenantId) || $tenantId === 'common') && !empty($saved['tenant_id']) && $saved['tenant_id'] !== 'common') { + $tenantId = trim($saved['tenant_id']); + } + if (!empty($clientId) && !empty($clientSecret) && !empty($tenantId) && $tenantId !== 'common') { + break; + } + } + } + + // Fallback: zoek een willekeurige OUTLOOK rij met ingevulde client_id + if (empty($clientId) || empty($clientSecret) || empty($tenantId) || $tenantId === 'common') { + $stmtFallback = $db->query(" + SELECT settings + FROM integrations + WHERE provider = 'OUTLOOK' AND settings LIKE '%client_id%' + ORDER BY updated_at DESC + LIMIT 3 "); - $rows = $stmt->fetchAll(); - foreach ($rows as $row) { - if (!empty($row['settings'])) { - $saved = json_decode($row['settings'], true) ?: []; + $fallbackRows = $stmtFallback->fetchAll(); + foreach ($fallbackRows as $fbRow) { + if (!empty($fbRow['settings'])) { + $saved = json_decode($fbRow['settings'], true) ?: []; if (empty($clientId) && !empty($saved['client_id'])) { $clientId = trim($saved['client_id']); } if (empty($clientSecret) && !empty($saved['client_secret'])) { $clientSecret = trim($saved['client_secret']); } - if (empty($tenantId) && !empty($saved['tenant_id'])) { + if ((empty($tenantId) || $tenantId === 'common') && !empty($saved['tenant_id']) && $saved['tenant_id'] !== 'common') { $tenantId = trim($saved['tenant_id']); } - if (!empty($clientId) && !empty($clientSecret)) { - break; - } } } - - // Fallback: zoek een willekeurige OUTLOOK rij met ingevulde client_id - if (empty($clientId) || empty($clientSecret)) { - $stmtFallback = $db->query(" - SELECT settings - FROM integrations - WHERE provider = 'OUTLOOK' AND settings LIKE '%client_id%' - ORDER BY updated_at DESC - LIMIT 3 - "); - $fallbackRows = $stmtFallback->fetchAll(); - foreach ($fallbackRows as $fbRow) { - if (!empty($fbRow['settings'])) { - $saved = json_decode($fbRow['settings'], true) ?: []; - if (empty($clientId) && !empty($saved['client_id'])) { - $clientId = trim($saved['client_id']); - } - if (empty($clientSecret) && !empty($saved['client_secret'])) { - $clientSecret = trim($saved['client_secret']); - } - if (empty($tenantId) && !empty($saved['tenant_id'])) { - $tenantId = trim($saved['tenant_id']); - } - if (!empty($clientId) && !empty($clientSecret)) { - break; - } - } - } - } - } catch (Throwable $e) { - error_log("microsoft_get_app_config error: " . $e->getMessage()); } + + // Als tenantId nog leeg of 'common' is, leid het domein af van de beheerder + if (empty($tenantId) || $tenantId === 'common') { + if (empty($adminOrgDomain)) { + $firstAdminEmail = $db->query("SELECT email FROM users WHERE role = 'ADMIN' ORDER BY id ASC LIMIT 1")->fetchColumn(); + if ($firstAdminEmail) { + $parts = explode('@', $firstAdminEmail); + $domain = strtolower(trim(end($parts))); + if (!in_array($domain, ['gmail.com', 'outlook.com', 'hotmail.com', 'live.com', 'yahoo.com', 'icloud.com'])) { + $adminOrgDomain = $domain; + } + } + } + if (!empty($adminOrgDomain)) { + $tenantId = $adminOrgDomain; + } + } + } catch (Throwable $e) { + error_log("microsoft_get_app_config error: " . $e->getMessage()); + } + + // Default tenantId voor de organisatie + if (empty($tenantId) || $tenantId === 'common') { + $tenantId = 'planningsagenda.nl'; } return [ 'client_id' => $clientId ?: '', 'client_secret' => $clientSecret ?: '', - 'tenant_id' => !empty($tenantId) ? $tenantId : 'common', + 'tenant_id' => $tenantId, 'is_configured' => !empty($clientId) && !empty($clientSecret), ]; } @@ -99,8 +125,8 @@ function microsoft_get_app_config(): array { /** * Genereert de Microsoft OAuth2 login URL */ -function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = 'common', string $state = ''): string { - $tenant = !empty($tenantId) ? trim($tenantId) : 'common'; +function microsoft_get_auth_url(string $clientId, string $redirectUri, string $tenantId = '', string $state = ''): string { + $tenant = (!empty($tenantId) && $tenantId !== 'common') ? trim($tenantId) : 'planningsagenda.nl'; $baseUrl = sprintf(MS_OAUTH_AUTHORIZE_URL, urlencode($tenant)); $params = [ @@ -119,8 +145,8 @@ function microsoft_get_auth_url(string $clientId, string $redirectUri, string $t /** * Wisselt de ontvangen autorisatiecode om voor access- en refresh tokens */ -function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = 'common'): array { - $tenant = !empty($tenantId) ? trim($tenantId) : 'common'; +function microsoft_exchange_code(string $code, string $clientId, string $clientSecret, string $redirectUri, string $tenantId = ''): array { + $tenant = (!empty($tenantId) && $tenantId !== 'common') ? trim($tenantId) : 'planningsagenda.nl'; $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($tenant)); $payload = [ @@ -210,7 +236,8 @@ function microsoft_get_valid_token(int $userId): ?string { return null; } - $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode(!empty($tenantId) ? $tenantId : 'common')); + $cleanTenant = (!empty($tenantId) && $tenantId !== 'common') ? $tenantId : 'planningsagenda.nl'; + $tokenUrl = sprintf(MS_OAUTH_TOKEN_URL, urlencode($cleanTenant)); $payload = [ 'client_id' => $clientId, 'client_secret' => $clientSecret,