--- /dev/null
+<?php
+// register.php - User registration with YubiKey WebAuthn
+
+ob_start();
+error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
+ini_set('display_errors', 1);
+ini_set('log_errors', 1);
+
+session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
+session_start();
+
+require_once __DIR__ . '/config/config.php';
+require_once __DIR__ . '/include/Database.php';
+require_once __DIR__ . '/include/WebAuthnManager.php';
+
+if (USER_LOGGED_IN) {
+ header("Location: index.php");
+ exit();
+}
+
+$db = new Database();
+$pdo = $db->connect();
+$webAuthnManager = new WebAuthnManager();
+$error = '';
+$success = '';
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ if (isset($_POST['username'])) {
+ $username = trim($_POST['username']);
+ if (!validate_username($username)) {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Nom d\'utilisateur invalide. 3-64 caractères (lettres, chiffres, _, -, @, .)']);
+ exit();
+ }
+ try {
+ $stmt = $pdo->prepare("SELECT user_id FROM users WHERE username = ?");
+ $stmt->execute([$username]);
+ if ($stmt->fetch()) {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Nom d\'utilisateur déjà pris']);
+ exit();
+ }
+ $registrationOptions = $webAuthnManager->generateRegistrationOptions($username);
+ $_SESSION['registration_options'] = $registrationOptions;
+ $_SESSION['registration_username'] = $username;
+ header('Content-Type: application/json');
+ echo json_encode(['success' => true, 'options' => $registrationOptions->jsonSerialize()]);
+ exit();
+ } catch (Exception $e) {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Erreur: ' . $e->getMessage()]);
+ exit();
+ }
+ } elseif (isset($_POST['attestationResponse'])) {
+ $attestationResponse = trim($_POST['attestationResponse']);
+ if (!empty($attestationResponse) && isset($_SESSION['registration_username'])) {
+ $username = $_SESSION['registration_username'];
+ unset($_SESSION['registration_username']);
+ $registrationData = $webAuthnManager->register($attestationResponse);
+ if ($registrationData && isset($registrationData['credentialId']) && isset($registrationData['publicKey'])) {
+ try {
+ $pdo->beginTransaction();
+ $stmt = $pdo->prepare("INSERT INTO users (username) VALUES (?) RETURNING user_id");
+ $stmt->execute([$username]);
+ $userId = $stmt->fetchColumn();
+ $stmt = $pdo->prepare("INSERT INTO yubikeys (user_id, credential_id, public_key, counter) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$userId, $registrationData['credentialId'], $registrationData['publicKey'], $registrationData['counter'] ?? 0]);
+ $yubikeyId = $pdo->lastInsertId();
+ $stmt = $pdo->prepare("UPDATE users SET yubikey_id = ? WHERE user_id = ?");
+ $stmt->execute([$yubikeyId, $userId]);
+ $pdo->commit();
+ $_SESSION['user_id'] = $userId;
+ $_SESSION['username'] = $username;
+ $_SESSION['status'] = "Inscription réussie ! Bienvenue $username";
+ $_SESSION['action_processed'] = false;
+ header('Content-Type: application/json');
+ echo json_encode(['success' => true, 'redirect' => 'index.php']);
+ exit();
+ } catch (Exception $e) {
+ if ($pdo->inTransaction()) $pdo->rollBack();
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Erreur: ' . $e->getMessage()]);
+ exit();
+ }
+ } else {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Réponse YubiKey invalide']);
+ exit();
+ }
+ } else {
+ header('Content-Type: application/json');
+ echo json_encode(['success' => false, 'error' => 'Session expirée']);
+ exit();
+ }
+ }
+}
+?>
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="mobile-web-app-capable" content="yes">
+ <title>Inscription - <?php echo APP_NAME; ?></title>
+ <style>
+ :root {--primary-color: #4a6fa5;--primary-dark: #166088;--secondary-color: #4fc3f7;--success-color: #4caf50;--error-color: #f44336;--background-color: #f5f7fa;--surface-color: #ffffff;--text-primary: #333333;--text-secondary: #666666;--border-color: #e0e0e0;--shadow: 0 2px 8px rgba(0,0,0,0.1);--border-radius: 8px;}
+ * {box-sizing: border-box; margin: 0; padding: 0;}
+ body {font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: var(--background-color); color: var(--text-primary); min-height: 100vh; display: flex; flex-direction: column;}
+ header {background: linear-gradient(135deg, var(--primary-dark), var(--primary-color)); color: white; padding: 16px 20px; box-shadow: var(--shadow);}
+ header h1 {font-size: 1.5rem; font-weight: 600;}
+ main {flex: 1; padding: 20px; max-width: 600px; margin: 0 auto; width: 100%;}
+ .register-form {background-color: var(--surface-color); border-radius: var(--border-radius); padding: 32px; box-shadow: var(--shadow);}
+ .register-form h2 {text-align: center; color: var(--primary-dark); margin-bottom: 24px; font-size: 1.5rem;}
+ .form-group {margin-bottom: 20px;}
+ .form-group label {display: block; margin-bottom: 6px; font-weight: 500;}
+ .form-group input[type="text"] {width: 100%; padding: 12px 14px; border: 2px solid var(--border-color); border-radius: var(--border-radius); font-size: 1rem; transition: all 0.2s;}
+ .form-group input[type="text"]:focus {outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(74, 111, 165, 0.1);}
+ button {padding: 12px 24px; border: none; border-radius: var(--border-radius); font-size: 1rem; font-weight: 500; cursor: pointer; transition: all 0.2s; display: block; width: 100%; margin-bottom: 12px;}
+ .btn-primary {background-color: var(--primary-color); color: white;}
+ .btn-primary:hover {background-color: var(--primary-dark);}
+ .btn-primary:disabled {background-color: #ccc; cursor: not-allowed;}
+ .message {padding: 12px 16px; border-radius: var(--border-radius); margin-bottom: 20px; font-weight: 500;}
+ .message.success {background-color: var(--success-color); color: white;}
+ .message.error {background-color: var(--error-color); color: white;}
+ .message.info {background-color: #e3f2fd; color: var(--primary-dark); border-left: 4px solid var(--secondary-color);}
+ #yubikeySection {display: none; margin-top: 24px; padding: 20px; background-color: #e3f2fd; border-radius: var(--border-radius); border-left: 4px solid var(--secondary-color); text-align: center;}
+ .yubikey-icon {font-size: 36px; margin-bottom: 8px;}
+ .browser-req {margin-top: 20px; padding: 12px; background-color: #fff3cd; border-radius: var(--border-radius); font-size: 0.85rem; color: #856404;}
+ .auth-links {text-align: center; margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--border-color);}
+ .auth-links a {color: var(--primary-color); text-decoration: none;}
+ .auth-links a:hover {text-decoration: underline;}
+ @media (min-width: 600px) {main {padding: 32px;} header h1 {font-size: 1.8rem;}}
+ </style>
+</head>
+<body>
+ <header><h1>Inscription</h1></header>
+ <main>
+ <div class="register-form">
+ <?php if ($error): ?><div class="message error"><?php echo htmlspecialchars($error); ?></div><?php endif; ?>
+ <?php if ($success): ?><div class="message success"><?php echo htmlspecialchars($success); ?></div><?php endif; ?>
+ <div class="message info"><strong>🔐</strong> Cette application utilise WebAuthn (YubiKey). Vous aurez besoin d'une clé compatible.</div>
+ <form method="post" id="registrationForm">
+ <div class="form-group"><label for="username">Nom d'utilisateur :</label>
+ <input type="text" id="username" name="username" required placeholder="3-64 caractères"></div>
+ <button type="submit" id="registerButton" class="btn-primary">S'inscrire</button>
+ </form>
+ <div id="yubikeySection"><div class="yubikey-icon">🔑</div><p>Veuillez toucher votre YubiKey.</p><div id="webauthnMessage"></div></div>
+ <div class="browser-req"><strong>⚠️</strong> HTTPS requis, navigateur moderne, clé WebAuthn/FIDO2.</div>
+ <div class="auth-links"><p>Déjà un compte ? <a href="index.php">Se connecter</a></p></div>
+ </div>
+ </main>
+ <footer style="text-align:center; padding:20px; color:var(--text-secondary); font-size:0.85rem;"><p>© 2026 - <?php echo APP_NAME; ?> | Nothing2Do.fr</p></footer>
+ <script>
+ function arrayBufferToBase64(buffer) {
+ return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+ }
+ document.addEventListener('DOMContentLoaded', function() {
+ const form = document.getElementById('registrationForm'), usernameInput = document.getElementById('username'),
+ registerButton = document.getElementById('registerButton'), yubikeySection = document.getElementById('yubikeySection'),
+ webauthnMessage = document.getElementById('webauthnMessage');
+ let registrationOptions = null;
+ form.addEventListener('submit', async function(e) {
+ e.preventDefault();
+ if (!usernameInput.value.trim()) { alert('Nom d\'utilisateur requis'); return; }
+ if (registrationOptions === null) {
+ try {
+ const response = await fetch('register.php', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+ body: 'username='+encodeURIComponent(usernameInput.value)});
+ const data = JSON.parse(await response.text());
+ if (data.success) {
+ registrationOptions = data.options; usernameInput.disabled = true; registerButton.disabled = true;
+ yubikeySection.style.display = 'block';
+ webauthnMessage.textContent = '✅ Prêt pour YubiKey...'; webauthnMessage.className = 'message success';
+ await startWebAuthnRegistration();
+ } else { webauthnMessage.textContent = data.error; webauthnMessage.className = 'message error'; webauthnMessage.style.display = 'block'; registerButton.disabled = false; }
+ } catch (error) { webauthnMessage.textContent = 'Erreur: ' + error.message; webauthnMessage.className = 'message error'; registerButton.disabled = false; }
+ }
+ });
+ async function startWebAuthnRegistration() {
+ try {
+ webauthnMessage.textContent = 'Touchez votre YubiKey...'; webauthnMessage.className = 'message info';
+ const publicKey = {
+ challenge: Uint8Array.from(atob(registrationOptions.challenge), c => c.charCodeAt(0)),
+ rp: registrationOptions.rp, user: registrationOptions.user,
+ pubKeyCredParams: registrationOptions.pubKeyCredParams,
+ authenticatorSelection: registrationOptions.authenticatorSelection,
+ timeout: registrationOptions.timeout, attestation: registrationOptions.attestation
+ };
+ const credential = await navigator.credentials.create({ publicKey });
+ if (credential) {
+ const attestationResponse = {id: credential.id, rawId: arrayBufferToBase64(credential.rawId),
+ response: {attestationObject: arrayBufferToBase64(credential.response.attestationObject),
+ clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON)}, type: credential.type};
+ const response = await fetch('register.php', {method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+ body: 'attestationResponse='+encodeURIComponent(JSON.stringify(attestationResponse))});
+ const data = JSON.parse(await response.text());
+ if (data.success) { webauthnMessage.textContent = 'Succès! Redirection...'; window.location.href = data.redirect; }
+ else { webauthnMessage.textContent = data.error; webauthnMessage.className = 'message error'; registerButton.disabled = false; }
+ } else { webauthnMessage.textContent = 'Aucune credential'; webauthnMessage.className = 'message error'; registerButton.disabled = false; }
+ } catch (error) { webauthnMessage.textContent = 'Erreur: ' + error.message; webauthnMessage.className = 'message error'; registerButton.disabled = false; }
+ }
+ });
+ </script>
+</body>
+</html>