dodanie csrf, oauth 2, dockerfile

This commit is contained in:
Marcin Cieślikowski
2026-03-16 10:43:19 +01:00
parent 56c7ecff6d
commit df448ea7c1
26 changed files with 916 additions and 318 deletions
+38 -4
View File
@@ -1,7 +1,41 @@
<?php
session_start();
if (session_status() !== PHP_SESSION_ACTIVE) {
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
session_set_cookie_params([
'httponly' => true,
'secure' => $isHttps,
'samesite' => 'Lax',
]);
session_start();
}
function e($value) {
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function getCsrfToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrfInput() {
return '<input type="hidden" name="csrf_token" value="' . e(getCsrfToken()) . '">';
}
function isValidCsrfToken($token) {
return is_string($token) && hash_equals(getCsrfToken(), $token);
}
function requireCsrfToken($token = null) {
if (!isValidCsrfToken($token ?? ($_POST['csrf_token'] ?? null))) {
http_response_code(403);
exit('Blad bezpieczenstwa: nieprawidlowy token formularza.');
}
}
// Funkcja sprawdzająca czy użytkownik jest zalogowany
function checkAuth() {
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
@@ -9,10 +43,10 @@ function checkAuth() {
}
}
// Funkcja sprawdzająca czy użytkownik jest adminem
function checkAdmin() {
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
die("Błąd: Brak uprawnień administratora.");
http_response_code(403);
exit('Blad: Brak uprawnien administratora.');
}
}
?>
+23 -10
View File
@@ -1,14 +1,27 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
ini_set('display_errors', '0');
ini_set('display_startup_errors', '0');
error_reporting(E_ALL);
// Dane do połączenia z bazą
define('DB_HOST', '');
define('DB_NAME', '');
define('DB_USER', ''); // Zmień na swojego użytkownika bazy
define('DB_PASS', ''); // Zmień na swoje hasło
define('DB_PREFIX', '');
// Ustawienia aplikacji
define('APP_NAME', ' Zeszyt 2.0');
function envOrDefault($name, $default = '') {
$value = getenv($name);
return $value !== false ? $value : $default;
}
define('DB_HOST', envOrDefault('DB_HOST', 'db'));
define('DB_NAME', envOrDefault('DB_NAME', 'zeszyt'));
define('DB_USER', envOrDefault('DB_USER', 'zeszyt'));
define('DB_PASS', envOrDefault('DB_PASS', 'zeszyt'));
define('DB_PREFIX', envOrDefault('DB_PREFIX', 'xyz_'));
define('APP_NAME', envOrDefault('APP_NAME', 'Zeszyt 2.0'));
// Przyklad redirect URI: http://localhost:8080/oauth_callback.php
define('MICROSOFT_OAUTH_ENABLED', filter_var(envOrDefault('MICROSOFT_OAUTH_ENABLED', 'false'), FILTER_VALIDATE_BOOLEAN));
define('MICROSOFT_OAUTH_TENANT', envOrDefault('MICROSOFT_OAUTH_TENANT', 'common'));
define('MICROSOFT_OAUTH_CLIENT_ID', envOrDefault('MICROSOFT_OAUTH_CLIENT_ID', ''));
define('MICROSOFT_OAUTH_CLIENT_SECRET', envOrDefault('MICROSOFT_OAUTH_CLIENT_SECRET', ''));
define('MICROSOFT_OAUTH_REDIRECT_URI', envOrDefault('MICROSOFT_OAUTH_REDIRECT_URI', ''));
define('MICROSOFT_OAUTH_ALLOWED_TENANT', envOrDefault('MICROSOFT_OAUTH_ALLOWED_TENANT', ''));
define('MICROSOFT_OAUTH_AUTO_PROVISION', filter_var(envOrDefault('MICROSOFT_OAUTH_AUTO_PROVISION', 'false'), FILTER_VALIDATE_BOOLEAN));
?>
+56
View File
@@ -10,7 +10,63 @@ try {
];
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
ensureOptionalSchema($pdo);
} catch (\PDOException $e) {
throw new \PDOException($e->getMessage(), (int)$e->getCode());
}
function ensureOptionalSchema(PDO $pdo) {
static $done = false;
if ($done || DB_PREFIX === '') {
return;
}
$done = true;
$usersTable = DB_PREFIX . "users";
try {
$columns = $pdo->query("SHOW COLUMNS FROM {$usersTable}")->fetchAll(PDO::FETCH_COLUMN, 0);
$indexes = $pdo->query("SHOW INDEX FROM {$usersTable}")->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
error_log($e->getMessage());
return;
}
$indexNames = array_column($indexes, 'Key_name');
$statements = [];
if (!in_array('email', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN email varchar(255) DEFAULT NULL AFTER username";
}
if (!in_array('display_name', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN display_name varchar(255) DEFAULT NULL AFTER email";
}
if (!in_array('oauth_provider', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN oauth_provider varchar(50) DEFAULT NULL AFTER role";
}
if (!in_array('oauth_subject', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN oauth_subject varchar(191) DEFAULT NULL AFTER oauth_provider";
}
if (!in_array('oauth_tenant_id', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN oauth_tenant_id varchar(64) DEFAULT NULL AFTER oauth_subject";
}
if (!in_array('last_login_at', $columns, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD COLUMN last_login_at datetime DEFAULT NULL AFTER oauth_tenant_id";
}
if (!in_array('uniq_users_email', $indexNames, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD UNIQUE KEY uniq_users_email (email)";
}
if (!in_array('idx_users_oauth_identity', $indexNames, true)) {
$statements[] = "ALTER TABLE {$usersTable} ADD KEY idx_users_oauth_identity (oauth_provider, oauth_subject, oauth_tenant_id)";
}
foreach ($statements as $statement) {
try {
$pdo->exec($statement);
} catch (\PDOException $e) {
error_log($e->getMessage());
}
}
}
?>
+293
View File
@@ -0,0 +1,293 @@
<?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];
}
?>