You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
9.7 KiB
PHP

<?php
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/db.php';
function isMicrosoftOAuthConfigured() {
return MICROSOFT_OAUTH_ENABLED
&& MICROSOFT_OAUTH_CLIENT_ID !== ''
&& MICROSOFT_OAUTH_CLIENT_SECRET !== ''
&& MICROSOFT_OAUTH_REDIRECT_URI !== '';
}
function getMicrosoftOAuthTenant() {
return MICROSOFT_OAUTH_TENANT !== '' ? MICROSOFT_OAUTH_TENANT : 'common';
}
function getMicrosoftOAuthBaseUrl() {
return 'https://login.microsoftonline.com/' . rawurlencode(getMicrosoftOAuthTenant()) . '/oauth2/v2.0';
}
function buildMicrosoftAuthorizationUrl() {
$state = bin2hex(random_bytes(32));
$nonce = bin2hex(random_bytes(32));
$_SESSION['microsoft_oauth_state'] = $state;
$_SESSION['microsoft_oauth_nonce'] = $nonce;
$params = [
'client_id' => 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('Nieprawidlowy format JWT');
}
$payload = json_decode(base64UrlDecode($parts[1]), true);
if (!is_array($payload)) {
throw new RuntimeException('Nie mozna odczytac 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('Nieprawidlowy nonce odpowiedzi Microsoft');
}
if ((string)($claims['aud'] ?? '') !== MICROSOFT_OAUTH_CLIENT_ID) {
throw new RuntimeException('Token Microsoft ma nieprawidlowe aud');
}
if ((int)($claims['exp'] ?? 0) < time()) {
throw new RuntimeException('Token Microsoft wygasl');
}
$tenantId = (string)($claims['tid'] ?? '');
$objectId = (string)($claims['oid'] ?? '');
if ($tenantId === '' || $objectId === '') {
throw new RuntimeException('Brak wymaganych claimow tid/oid');
}
$issuer = (string)($claims['iss'] ?? '');
if ($issuer === '' || strpos($issuer, 'https://login.microsoftonline.com/') !== 0) {
throw new RuntimeException('Token Microsoft ma nieprawidlowe 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 powiazane z zadnym lokalnym uzytkownikiem');
}
$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('Blad 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 mozna polaczyc sie 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];
}
?>