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
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];
|
|
}
|
|
?>
|