MICROSOFT_OAUTH_CLIENT_ID, 'response_type' => 'code', 'redirect_uri' => MICROSOFT_OAUTH_REDIRECT_URI, 'response_mode' => 'query', 'scope' => 'openid profile email offline_access', 'state' => $state, 'nonce' => $nonce, ]; return getMicrosoftOAuthBaseUrl() . '/authorize?' . http_build_query($params); } function exchangeMicrosoftCodeForTokens($code) { $response = httpPostForm( getMicrosoftOAuthBaseUrl() . '/token', [ 'client_id' => MICROSOFT_OAUTH_CLIENT_ID, 'client_secret' => MICROSOFT_OAUTH_CLIENT_SECRET, 'grant_type' => 'authorization_code', 'code' => $code, 'redirect_uri' => MICROSOFT_OAUTH_REDIRECT_URI, ] ); if ($response['status'] < 200 || $response['status'] >= 300) { throw new RuntimeException('Microsoft token endpoint returned HTTP ' . $response['status']); } $payload = json_decode($response['body'], true); if (!is_array($payload) || empty($payload['id_token'])) { throw new RuntimeException('Brak id_token w odpowiedzi Microsoft'); } return $payload; } function decodeJwtPayload($jwt) { $parts = explode('.', $jwt); if (count($parts) < 2) { throw new RuntimeException('Nieprawidłowy format JWT'); } $payload = json_decode(base64UrlDecode($parts[1]), true); if (!is_array($payload)) { throw new RuntimeException('Nie można odczytać payload JWT'); } return $payload; } function base64UrlDecode($data) { $remainder = strlen($data) % 4; if ($remainder > 0) { $data .= str_repeat('=', 4 - $remainder); } return base64_decode(strtr($data, '-_', '+/')); } function extractMicrosoftIdentity(array $tokenPayload) { $claims = decodeJwtPayload($tokenPayload['id_token']); $expectedNonce = $_SESSION['microsoft_oauth_nonce'] ?? null; unset($_SESSION['microsoft_oauth_nonce']); if (!$expectedNonce || !hash_equals($expectedNonce, (string)($claims['nonce'] ?? ''))) { throw new RuntimeException('Nieprawidłowy nonce odpowiedzi Microsoft'); } if ((string)($claims['aud'] ?? '') !== MICROSOFT_OAUTH_CLIENT_ID) { throw new RuntimeException('Token Microsoft ma nieprawidłowe aud'); } if ((int)($claims['exp'] ?? 0) < time()) { throw new RuntimeException('Token Microsoft wygasł'); } $tenantId = (string)($claims['tid'] ?? ''); $objectId = (string)($claims['oid'] ?? ''); if ($tenantId === '' || $objectId === '') { throw new RuntimeException('Brak wymaganych claimów tid/oid'); } $issuer = (string)($claims['iss'] ?? ''); if ($issuer === '' || strpos($issuer, 'https://login.microsoftonline.com/') !== 0) { throw new RuntimeException('Token Microsoft ma nieprawidłowe iss'); } if (MICROSOFT_OAUTH_ALLOWED_TENANT !== '' && !hash_equals(MICROSOFT_OAUTH_ALLOWED_TENANT, $tenantId)) { throw new RuntimeException('Ten tenant Microsoft nie jest dozwolony'); } $email = trim((string)($claims['email'] ?? '')); $preferredUsername = trim((string)($claims['preferred_username'] ?? '')); if ($email === '' && filter_var($preferredUsername, FILTER_VALIDATE_EMAIL)) { $email = $preferredUsername; } return [ 'tenant_id' => $tenantId, 'subject' => $objectId, 'display_name' => trim((string)($claims['name'] ?? '')), 'email' => $email, 'preferred_username' => $preferredUsername, ]; } function findOrProvisionMicrosoftUser(PDO $pdo, array $identity) { $lookup = $pdo->prepare( "SELECT * FROM " . DB_PREFIX . "users WHERE oauth_provider = 'microsoft' AND oauth_subject = ? AND oauth_tenant_id = ? LIMIT 1" ); $lookup->execute([$identity['subject'], $identity['tenant_id']]); $user = $lookup->fetch(); if ($user) { return updateMicrosoftUserProfile($pdo, $user, $identity); } $candidate = $identity['email'] !== '' ? $identity['email'] : $identity['preferred_username']; if ($candidate !== '') { $link = $pdo->prepare( "SELECT * FROM " . DB_PREFIX . "users WHERE email = ? OR username = ? LIMIT 1" ); $link->execute([$candidate, $candidate]); $user = $link->fetch(); if ($user) { $stmt = $pdo->prepare( "UPDATE " . DB_PREFIX . "users SET email = COALESCE(email, ?), display_name = ?, oauth_provider = 'microsoft', oauth_subject = ?, oauth_tenant_id = ?, last_login_at = NOW() WHERE id = ?" ); $stmt->execute([ $identity['email'] !== '' ? $identity['email'] : null, $identity['display_name'] !== '' ? $identity['display_name'] : null, $identity['subject'], $identity['tenant_id'], $user['id'], ]); $reload = $pdo->prepare("SELECT * FROM " . DB_PREFIX . "users WHERE id = ?"); $reload->execute([$user['id']]); return $reload->fetch(); } } if (!MICROSOFT_OAUTH_AUTO_PROVISION) { throw new RuntimeException('To konto Microsoft nie jest powiązane z żadnym lokalnym użytkownikiem'); } $usernameBase = $candidate !== '' ? $candidate : 'm365_' . substr($identity['subject'], 0, 12); $username = makeUniqueUsername($pdo, $usernameBase); $passwordHash = password_hash(bin2hex(random_bytes(24)), PASSWORD_BCRYPT); $insert = $pdo->prepare( "INSERT INTO " . DB_PREFIX . "users (username, email, display_name, password, role, oauth_provider, oauth_subject, oauth_tenant_id, last_login_at) VALUES (?, ?, ?, ?, 'user', 'microsoft', ?, ?, NOW())" ); $insert->execute([ $username, $identity['email'] !== '' ? $identity['email'] : null, $identity['display_name'] !== '' ? $identity['display_name'] : null, $passwordHash, $identity['subject'], $identity['tenant_id'], ]); $reload = $pdo->prepare("SELECT * FROM " . DB_PREFIX . "users WHERE id = ?"); $reload->execute([$pdo->lastInsertId()]); return $reload->fetch(); } function updateMicrosoftUserProfile(PDO $pdo, array $user, array $identity) { $stmt = $pdo->prepare( "UPDATE " . DB_PREFIX . "users SET email = COALESCE(?, email), display_name = COALESCE(?, display_name), last_login_at = NOW() WHERE id = ?" ); $stmt->execute([ $identity['email'] !== '' ? $identity['email'] : null, $identity['display_name'] !== '' ? $identity['display_name'] : null, $user['id'], ]); $reload = $pdo->prepare("SELECT * FROM " . DB_PREFIX . "users WHERE id = ?"); $reload->execute([$user['id']]); return $reload->fetch(); } function makeUniqueUsername(PDO $pdo, $base) { $base = preg_replace('/[^a-zA-Z0-9._@-]/', '_', $base); $base = trim($base, '_'); if ($base === '') { $base = 'user'; } $candidate = $base; $i = 1; $stmt = $pdo->prepare("SELECT COUNT(*) FROM " . DB_PREFIX . "users WHERE username = ?"); while (true) { $stmt->execute([$candidate]); if ((int)$stmt->fetchColumn() === 0) { return $candidate; } $i++; $candidate = $base . '_' . $i; } } function loginUserIntoSession(array $user) { session_regenerate_id(true); $_SESSION['user_id'] = $user['id']; $_SESSION['username'] = $user['username']; $_SESSION['role'] = $user['role']; } function httpPostForm($url, array $fields) { $body = http_build_query($fields); if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'], CURLOPT_TIMEOUT => 15, ]); $responseBody = curl_exec($ch); if ($responseBody === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException('Błąd cURL: ' . $error); } $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return ['status' => $status, 'body' => $responseBody]; } $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/x-www-form-urlencoded\r\n", 'content' => $body, 'timeout' => 15, 'ignore_errors' => true, ], ]); $responseBody = file_get_contents($url, false, $context); if ($responseBody === false) { throw new RuntimeException('Nie można połączyć się z Microsoft'); } $status = 0; foreach ($http_response_header ?? [] as $header) { if (preg_match('#HTTP/\S+\s+(\d+)#', $header, $matches)) { $status = (int)$matches[1]; break; } } return ['status' => $status, 'body' => $responseBody]; } ?>