<?php /** * PA2JFX Login Handler v5 — Cookie-based + server-side token */ $pa2jfx_allowed_emails = array('jkoolschijn@pa2jfx.nl'); $pa2jfx_ip_whitelist = array('87.106.147.188', '::1'); $pa2jfx_max_attempts = 3; function pa2jfx_security_log($event, $detail = '') { global $wpdb; $table = $wpdb->prefix . 'pa2jfx_security_log'; $wpdb->query("CREATE TABLE IF NOT EXISTS $table ( id bigint(20) NOT NULL AUTO_INCREMENT, event varchar(100) NOT NULL, ip varchar(45) NOT NULL DEFAULT '', email varchar(100) NOT NULL DEFAULT '', detail text, created_at datetime NOT NULL, PRIMARY KEY (id), KEY event (event) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); $email = $_COOKIE['pa2jfx_otp_email'] ?? ''; $wpdb->insert($table, array( 'event' => $event, 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'email' => $email, 'detail' => $detail, 'created_at' => current_time('mysql'), )); } function pa2jfx_ip_whitelisted() { global $pa2jfx_ip_whitelist; return in_array($_SERVER['REMOTE_ADDR'] ?? '', $pa2jfx_ip_whitelist); } function pa2jfx_ip_blocked() { global $pa2jfx_max_attempts; if (pa2jfx_ip_whitelisted()) return false; return (int) get_transient('pa2jfx_b_' . md5($_SERVER['REMOTE_ADDR'])) >= $pa2jfx_max_attempts; } function pa2jfx_ip_increment() { if (pa2jfx_ip_whitelisted()) return; $key = 'pa2jfx_b_' . md5($_SERVER['REMOTE_ADDR']); set_transient($key, (int) get_transient($key) + 1, 86400); } function pa2jfx_allowed_email($email) { global $pa2jfx_allowed_emails; return in_array(strtolower(trim($email)), array_map('strtolower', $pa2jfx_allowed_emails)); } class PA2JFX_TOTP { private $secret; public function __construct($s = null) { $this->secret = $s ?: $this->gen(); } private function gen() { $c='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';$s='';for($i=0;$i<32;$i++)$s.=$c[random_int(0,31)];return $s; } public function get_secret() { return $this->secret; } public function verify($code, $w=1) { $code = preg_replace('/[^0-9]/','',(string)$code); if(strlen($code)!==6)return false; $d = $this->b32d($this->secret); $t = floor(time()/30); for($i=-$w;$i<=$w;$i++) { $h = hash_hmac('sha1', pack('N*',0).pack('N*',$t+$i), $d, true); $o = ord($h[19])&0x0F; $b = ((ord($h[$o])&0x7F)<<24)|((ord($h[$o+1])&0xFF)<<16)|((ord($h[$o+2])&0xFF)<<8)|(ord($h[$o+3])&0xFF); if(sprintf('%06d',$b%1000000)===$code) return true; } return false; } private function b32d($d) { $c='ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';$d=strtoupper($d);$b=0;$n=0;$r=''; for($i=0;$i=8){$r.=chr(($b>>($n-8))&0xFF);$n-=8;}} return $r; } } ​ function pa2jfx_generate_otp($email) { if (!pa2jfx_allowed_email($email)) { setcookie('pa2jfx_otp_sent', '1', time()+300, '/', '', true, true); return false; } if (pa2jfx_ip_blocked()) { return false; } $code = sprintf('%06d', random_int(0, 999999)); $token = wp_hash($email . time() . rand()); set_transient('pa2jfx_otp_' . $token, [ 'code' => password_hash($code, PASSWORD_DEFAULT), 'email' => sanitize_email($email), 'expires' => time() + 300, 'attempts' => 0 ], 300); wp_mail($email, 'PA2JFX — Uw inlogcode', "Uw eenmalige inlogcode:\n\n$code\n\nGeldig: 5 minuten\n73 — PA2JFX", ['Content-Type: text/plain; charset=UTF-8']); setcookie('pa2jfx_otp_token', $token, time()+300, '/', '', true, true); setcookie('pa2jfx_otp_email', $email, time()+300, '/', '', true, true); setcookie('pa2jfx_otp_sent', '1', time()+300, '/', '', true, true); pa2jfx_security_log('otp_sent', "Code naar $email"); return true; } function pa2jfx_verify_otp($code) { $token = $_COOKIE['pa2jfx_otp_token'] ?? ''; if (!$token) return false; $data = get_transient('pa2jfx_otp_' . $token); if (!$data || $data['expires'] < time()) return false; if ($data['attempts'] > 3) { pa2jfx_ip_increment(); return false; } if (password_verify($code, $data['code'])) { pa2jfx_security_log('otp_ok', 'Code OK'); $email = $data['email']; $secret = get_option('pa2jfx_totp_' . md5($email), ''); if (!empty($secret)) { setcookie('pa2jfx_totp_pending', '1', time()+300, '/', '', true, true); return 'totp_needed'; } return pa2jfx_do_login($email); } $data['attempts']++; set_transient('pa2jfx_otp_' . $token, $data, 300); if ($data['attempts'] >= 3) pa2jfx_ip_increment(); return false; } function pa2jfx_verify_totp($code) { $email = $_COOKIE['pa2jfx_otp_email'] ?? ''; $secret = get_option('pa2jfx_totp_' . md5($email), ''); if (!$secret) return pa2jfx_do_login($email); $totp = new PA2JFX_TOTP($secret); if ($totp->verify($code, 2)) { pa2jfx_security_log('login_success', 'TOTP+OTP OK'); return pa2jfx_do_login($email); } pa2jfx_security_log('totp_failed', 'Foute TOTP'); return false; } // ====== VEILIGE SESSIE (server-side) ====== function pa2jfx_do_login($email) { $token = bin2hex(random_bytes(32)); set_transient('pa2jfx_session_' . $token, [ 'email' => $email, 'time' => time(), 'ip' => $_SERVER['REMOTE_ADDR'] ?? '' ], 86400); setcookie('pa2jfx_sid', $token, time()+86400, '/', '', true, true); // Opruimen OTP-cookies foreach (['pa2jfx_otp_token','pa2jfx_otp_email','pa2jfx_otp_sent','pa2jfx_totp_pending'] as $c) setcookie($c, '', time()-3600, '/', '', true, true); pa2jfx_security_log('login', "$email ingelogd"); return true; } function pa2jfx_is_logged_in() { $sid = $_COOKIE['pa2jfx_sid'] ?? ''; if (!$sid) return false; return get_transient('pa2jfx_session_' . $sid) !== false; } function pa2jfx_logout() { $sid = $_COOKIE['pa2jfx_sid'] ?? ''; if ($sid) delete_transient('pa2jfx_session_' . $sid); setcookie('pa2jfx_sid', '', time()-3600, '/', '', true, true); } function pa2jfx_handle_login_actions() { if (!isset($_POST['pa2jfx_action'])) return; switch ($_POST['pa2jfx_action']) { case 'request_otp': $email = sanitize_email($_POST['pa2jfx_email'] ?? ''); if (is_email($email)) pa2jfx_generate_otp($email); break; case 'verify_otp': $code = preg_replace('/[^0-9]/', '', $_POST['pa2jfx_code'] ?? ''); if (strlen($code) === 6) { $result = pa2jfx_verify_otp($code); if ($result === 'totp_needed') setcookie('pa2jfx_totp_pending', '1', time()+300, '/', '', true, true); } break; case 'verify_totp': $code = preg_replace('/[^0-9]/', '', $_POST['pa2jfx_totp'] ?? ''); if (strlen($code) === 6) pa2jfx_verify_totp($code); break; case 'logout': pa2jfx_logout(); break; } } function pa2jfx_login_form() { $lang = isset($_COOKIE['pa2jfx_lang']) && $_COOKIE['pa2jfx_lang'] === 'en' ? 'en' : 'nl'; // Lees status uit cookies $otp_token = $_COOKIE['pa2jfx_otp_token'] ?? ''; $otp_sent = $_COOKIE['pa2jfx_otp_sent'] ?? ''; $totp_pending = $_COOKIE['pa2jfx_totp_pending'] ?? ''; $error = $_COOKIE['pa2jfx_otp_error'] ?? ''; $blocked = isset($_COOKIE['pa2jfx_otp_blocked']) ? '1' : (pa2jfx_ip_blocked() ? '1' : ''); // Error cookie wissen na uitlezen if ($error) setcookie('pa2jfx_otp_error', '', time()-3600, '/', '', true, true); if ($blocked) setcookie('pa2jfx_otp_blocked', '', time()-3600, '/', '', true, true); // Bepaal of we code-veld moeten tonen: token bestaat OF otp_sent is gezet $show_code = $otp_token || $otp_sent; ?> PA2JFX .cookie-popup .btn-accept:hover{background:#33a0ad} PA2JFX