Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
bb7b039a32 |
419
WP Multi 2FA.php
Normal file
419
WP Multi 2FA.php
Normal file
@@ -0,0 +1,419 @@
|
||||
<?php
|
||||
/*
|
||||
* Plugin Name: WP Multi 2FA
|
||||
* Plugin URI: https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA
|
||||
* Description: 2FA mit Backup-Codes und Admin-Erzwingung.
|
||||
* Version: 1.0
|
||||
* Author: M_Viper
|
||||
* Author URI: https://m-viper.de
|
||||
* Requires at least: 6.7.2
|
||||
* Tested up to: 6.7.2
|
||||
* PHP Version: 7.2
|
||||
* License: GPL2
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Support: [Microsoft Teams Support](https://teams.live.com/l/community/FEAzokphpZTJ2u6OgI)
|
||||
* Support: [Telegram Support](https://t.me/M_Viper04)
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
// === 1. Plugin-Zeile in der Plugin-Liste erweitern ===
|
||||
add_filter('plugin_row_meta', 'wp_multi_2fa_plugin_meta_links', 10, 2);
|
||||
function wp_multi_2fa_plugin_meta_links($links, $file) {
|
||||
if (plugin_basename(__FILE__) === $file) {
|
||||
$update_link = '<a href="https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA/releases" target="_blank" style="color: #d63638; font-weight: bold;">🔴 Neue Updates auf Gitea</a>';
|
||||
$links[] = $update_link;
|
||||
}
|
||||
return $links;
|
||||
}
|
||||
|
||||
// === 2. Admin-Dashboard-Hinweis bei neuer Version ===
|
||||
add_action('admin_init', 'wp_multi_2fa_check_update_notice');
|
||||
function wp_multi_2fa_check_update_notice() {
|
||||
if (!current_user_can('update_plugins')) return;
|
||||
|
||||
$transient = get_transient('wp_multi_2fa_gitea_version');
|
||||
|
||||
if ($transient === false) {
|
||||
$response = wp_remote_get('https://git.viper.ipv64.net/api/v1/repos/M_Viper/WP-Multi-2FA/releases/latest');
|
||||
if (is_wp_error($response)) return;
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($response));
|
||||
if (isset($data->tag_name)) {
|
||||
$latest = ltrim($data->tag_name, 'v'); // Entfernt evtl. "v" vor der Versionsnummer
|
||||
set_transient('wp_multi_2fa_gitea_version', $latest, 12 * HOUR_IN_SECONDS);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$latest = $transient;
|
||||
}
|
||||
|
||||
if (!function_exists('get_plugin_data')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$plugin_data = get_plugin_data(__FILE__);
|
||||
$current_version = $plugin_data['Version'];
|
||||
|
||||
if (version_compare($latest, $current_version, '>')) {
|
||||
add_action('admin_notices', function () use ($latest) {
|
||||
?>
|
||||
<div class="notice notice-warning is-dismissible">
|
||||
<p><strong>WP Multi 2FA:</strong> Eine neue Version (<?= esc_html($latest); ?>) ist verfügbar.
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/WP-Multi-2FA/releases" target="_blank">Jetzt ansehen auf Gitea</a>.</p>
|
||||
</div>
|
||||
<?php
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class WP_2FA_Plugin {
|
||||
private $option_name = 'wp2fa_users';
|
||||
|
||||
public function __construct() {
|
||||
add_action('show_user_profile', [$this, 'show_2fa_settings']);
|
||||
add_action('edit_user_profile', [$this, 'show_2fa_settings']);
|
||||
add_action('personal_options_update', [$this, 'save_2fa_settings']);
|
||||
add_action('edit_user_profile_update', [$this, 'save_2fa_settings']);
|
||||
|
||||
// Login flow
|
||||
add_action('wp_login', [$this, 'after_login'], 10, 2);
|
||||
add_action('init', [$this, 'handle_2fa']);
|
||||
|
||||
// Einstellungen > Allgemein
|
||||
add_action('admin_init', [$this, 'register_settings_field']);
|
||||
|
||||
// Session für 2FA-Code speichern
|
||||
if (!session_id()) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
private function get_user_2fa_data($user_id) {
|
||||
$all = get_option($this->option_name, []);
|
||||
return $all[$user_id] ?? null;
|
||||
}
|
||||
|
||||
private function set_user_2fa_data($user_id, $data) {
|
||||
$all = get_option($this->option_name, []);
|
||||
$all[$user_id] = $data;
|
||||
update_option($this->option_name, $all);
|
||||
}
|
||||
|
||||
public function show_2fa_settings($user) {
|
||||
if (!current_user_can('edit_user', $user->ID)) return;
|
||||
|
||||
$data = $this->get_user_2fa_data($user->ID);
|
||||
$enabled = $data['enabled'] ?? false;
|
||||
$secret = $data['secret'] ?? null;
|
||||
$backup_codes = $data['backup_codes'] ?? [];
|
||||
|
||||
if (!$secret && $enabled) {
|
||||
$secret = $this->generate_secret();
|
||||
$data['secret'] = $secret;
|
||||
$backup_codes = $this->generate_backup_codes();
|
||||
$data['backup_codes'] = $backup_codes;
|
||||
$this->set_user_2fa_data($user->ID, $data);
|
||||
}
|
||||
|
||||
?>
|
||||
<h2>2-Faktor-Authentifizierung</h2>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th><label for="wp2fa_enabled">2FA aktivieren</label></th>
|
||||
<td>
|
||||
<input type="checkbox" name="wp2fa_enabled" id="wp2fa_enabled" value="1" <?php checked($enabled); ?>>
|
||||
<p class="description">Aktiviere die Zwei-Faktor-Authentifizierung für dein Konto.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if ($enabled && $secret): ?>
|
||||
<tr>
|
||||
<th>QR-Code (für Authenticator-App)</th>
|
||||
<td>
|
||||
<img src="<?php echo esc_url($this->get_qr_code_url($user->user_email, $secret)); ?>" alt="QR Code" />
|
||||
<p>Scanne diesen QR-Code mit einer App wie Google Authenticator oder Authy.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Backup-Codes</th>
|
||||
<td>
|
||||
<textarea readonly rows="5" cols="30" style="font-family: monospace;"><?php echo implode("\n", $backup_codes); ?></textarea><br>
|
||||
<button type="button" onclick="downloadBackupCodes()">Backup-Codes herunterladen</button>
|
||||
<script>
|
||||
function downloadBackupCodes() {
|
||||
const codes = <?php echo json_encode($backup_codes); ?>;
|
||||
const blob = new Blob([codes.join("\n")], {type: 'text/plain'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'backup-codes.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
<p>Bewahre diese Codes sicher auf. Jeder Code kann einmal zur Anmeldung verwendet werden.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function save_2fa_settings($user_id) {
|
||||
if (!current_user_can('edit_user', $user_id)) return;
|
||||
|
||||
$data = $this->get_user_2fa_data($user_id) ?? [];
|
||||
|
||||
$enabled = isset($_POST['wp2fa_enabled']) && $_POST['wp2fa_enabled'] == '1';
|
||||
|
||||
if ($enabled && empty($data['secret'])) {
|
||||
$data['secret'] = $this->generate_secret();
|
||||
$data['backup_codes'] = $this->generate_backup_codes();
|
||||
}
|
||||
|
||||
if (!$enabled) {
|
||||
// Deaktivieren: löschen
|
||||
$data = [];
|
||||
} else {
|
||||
$data['enabled'] = true;
|
||||
}
|
||||
|
||||
$this->set_user_2fa_data($user_id, $data);
|
||||
}
|
||||
|
||||
private function generate_secret() {
|
||||
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
$secret = '';
|
||||
for ($i=0; $i < 16; $i++) {
|
||||
$secret .= $chars[random_int(0, 31)];
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
|
||||
private function generate_backup_codes() {
|
||||
$codes = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$codes[] = strtoupper(bin2hex(random_bytes(4)));
|
||||
}
|
||||
return $codes;
|
||||
}
|
||||
|
||||
private function get_qr_code_url($email, $secret) {
|
||||
$issuer = rawurlencode(get_bloginfo('name'));
|
||||
$label = rawurlencode(get_bloginfo('name') . ':' . $email);
|
||||
$otpauth = "otpauth://totp/{$label}?secret={$secret}&issuer={$issuer}";
|
||||
$chl = urlencode($otpauth);
|
||||
return "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={$chl}";
|
||||
}
|
||||
|
||||
// Nach Login prüfen, ob 2FA nötig
|
||||
public function after_login($user_login, $user) {
|
||||
if (!$this->needs_2fa($user->ID)) return;
|
||||
|
||||
$_SESSION['wp2fa_user'] = $user->ID;
|
||||
wp_logout();
|
||||
wp_redirect(site_url('/wp-login.php?action=wp2fa'));
|
||||
exit;
|
||||
}
|
||||
|
||||
private function needs_2fa($user_id) {
|
||||
$data = $this->get_user_2fa_data($user_id);
|
||||
$admin_forced = get_option('wp2fa_admin_forced', false);
|
||||
return ($data['enabled'] ?? false) || $admin_forced;
|
||||
}
|
||||
|
||||
// 2FA Handler: Formular verarbeiten und ggf. anzeigen (Popup)
|
||||
public function handle_2fa() {
|
||||
if (!isset($_GET['action']) || $_GET['action'] !== 'wp2fa') return;
|
||||
|
||||
$user_id = $_SESSION['wp2fa_user'] ?? 0;
|
||||
if (!$user_id) {
|
||||
wp_redirect(wp_login_url());
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$code = strtoupper(trim($_POST['wp2fa_code'] ?? ''));
|
||||
|
||||
$data = $this->get_user_2fa_data($user_id);
|
||||
if (!$data) {
|
||||
wp_redirect(wp_login_url());
|
||||
exit;
|
||||
}
|
||||
|
||||
$secret = $data['secret'] ?? '';
|
||||
$backup_codes = $data['backup_codes'] ?? [];
|
||||
|
||||
if ($this->verify_code($secret, $code)) {
|
||||
// Erfolgreicher Authenticator-Code
|
||||
wp_set_auth_cookie($user_id);
|
||||
unset($_SESSION['wp2fa_user']);
|
||||
wp_redirect(admin_url());
|
||||
exit;
|
||||
} elseif (in_array($code, $backup_codes, true)) {
|
||||
// Backup-Code erfolgreich, diesen Code löschen
|
||||
$backup_codes = array_diff($backup_codes, [$code]);
|
||||
$data['backup_codes'] = $backup_codes;
|
||||
$this->set_user_2fa_data($user_id, $data);
|
||||
|
||||
wp_set_auth_cookie($user_id);
|
||||
unset($_SESSION['wp2fa_user']);
|
||||
wp_redirect(admin_url());
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Falscher Code!';
|
||||
}
|
||||
}
|
||||
|
||||
$this->show_2fa_form($error);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function show_2fa_form($error = '') {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>2-Faktor-Authentifizierung</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #222;
|
||||
margin: 0; padding: 0; height: 100vh;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
#wp2fa-modal {
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.5);
|
||||
width: 320px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
#wp2fa-modal h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#wp2fa-modal input[type="text"] {
|
||||
font-size: 18px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
letter-spacing: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
#wp2fa-modal button {
|
||||
background-color: #0073aa;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#wp2fa-modal .error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wp2fa-modal" role="dialog" aria-modal="true" aria-labelledby="wp2fa-title">
|
||||
<h1 id="wp2fa-title">2-Faktor-Authentifizierung</h1>
|
||||
<?php if ($error): ?>
|
||||
<div class="error"><?php echo esc_html($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="post">
|
||||
<label for="wp2fa_code">Gib deinen Authenticator- oder Backup-Code ein:</label><br>
|
||||
<input type="text" name="wp2fa_code" id="wp2fa_code" autocomplete="one-time-code" required maxlength="8" autofocus><br>
|
||||
<button type="submit">Bestätigen</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function verify_code($secret, $code) {
|
||||
// TOTP-Verifikation (30-Sekunden Fenster)
|
||||
$timeSlice = floor(time() / 30);
|
||||
for ($i = -1; $i <= 1; $i++) {
|
||||
$calc = $this->get_totp_code($secret, $timeSlice + $i);
|
||||
if ($calc === $code) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function get_totp_code($secret, $timeSlice) {
|
||||
$secretkey = $this->base32_decode($secret);
|
||||
|
||||
$time = pack('N*', 0) . pack('N*', $timeSlice);
|
||||
$hash = hash_hmac('sha1', $time, $secretkey, true);
|
||||
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||
$truncatedHash = substr($hash, $offset, 4);
|
||||
|
||||
$code = unpack('N', $truncatedHash)[1] & 0x7FFFFFFF;
|
||||
$code = $code % 1000000;
|
||||
|
||||
return str_pad($code, 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function base32_decode($b32) {
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||
$b32 = strtoupper($b32);
|
||||
$l = strlen($b32);
|
||||
$n = 0;
|
||||
$j = 0;
|
||||
$binary = '';
|
||||
|
||||
for ($i = 0; $i < $l; $i++) {
|
||||
$n = $n << 5; // 5 bits per char
|
||||
$n = $n + strpos($alphabet, $b32[$i]);
|
||||
$j += 5;
|
||||
if ($j >= 8) {
|
||||
$j -= 8;
|
||||
$binary .= chr(($n & (0xFF << $j)) >> $j);
|
||||
}
|
||||
}
|
||||
return $binary;
|
||||
}
|
||||
|
||||
// Registrierung des Admin-Feldes in Einstellungen > Allgemein
|
||||
public function register_settings_field() {
|
||||
register_setting('general', 'wp2fa_admin_forced', [
|
||||
'type' => 'boolean',
|
||||
'description' => '2FA für alle Benutzer erzwingen',
|
||||
'sanitize_callback' => [$this, 'sanitize_bool'],
|
||||
'default' => false,
|
||||
]);
|
||||
|
||||
add_settings_field(
|
||||
'wp2fa_admin_forced',
|
||||
'2FA für alle Benutzer erzwingen',
|
||||
[$this, 'render_admin_forced_field'],
|
||||
'general'
|
||||
);
|
||||
}
|
||||
|
||||
public function render_admin_forced_field() {
|
||||
$forced = get_option('wp2fa_admin_forced', false);
|
||||
?>
|
||||
<input type="checkbox" id="wp2fa_admin_forced" name="wp2fa_admin_forced" value="1" <?php checked($forced, true); ?> />
|
||||
<label for="wp2fa_admin_forced">Wenn aktiviert, müssen alle Benutzer 2FA nutzen, auch wenn sie es nicht selbst aktiviert haben.</label>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function sanitize_bool($value) {
|
||||
return ($value === '1' || $value === 1 || $value === true) ? true : false;
|
||||
}
|
||||
}
|
||||
|
||||
new WP_2FA_Plugin();
|
Reference in New Issue
Block a user