Overview
Comment: | Refactor password change form, customize for first password |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | dev |
Files: | files | file ages | folders |
SHA3-256: |
ff4431861d9c76a44b4c56e58f6b8954 |
User & Date: | bohwaz on 2022-08-13 12:04:31 |
Other Links: | branch diff | manifest | tags |
Context
2022-08-13
| ||
20:22 | Refactor user security details page, login and password recovery check-in: e4a64ff99c user: bohwaz tags: dev | |
12:04 | Refactor password change form, customize for first password check-in: ff4431861d user: bohwaz tags: dev | |
10:20 | Merge with trunk check-in: cb79a7bfc6 user: bohwaz tags: dev | |
Changes
Modified src/include/data/1.2.0_schema.sql from [9e6714862a] to [18fc632152].
︙ | ︙ | |||
106 107 108 109 110 111 112 | CREATE TABLE IF NOT EXISTS users_sessions -- Permanent sessions for logged-in users ( selector TEXT NOT NULL, hash TEXT NOT NULL, id_user INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, | | | 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | CREATE TABLE IF NOT EXISTS users_sessions -- Permanent sessions for logged-in users ( selector TEXT NOT NULL, hash TEXT NOT NULL, id_user INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, expiry INT NOT NULL, PRIMARY KEY (selector, id_user) ); CREATE TABLE IF NOT EXISTS logs ( id INTEGER NOT NULL PRIMARY KEY, |
︙ | ︙ |
Modified src/include/lib/Garradin/Template.php from [afb4864343] to [158689978f].
︙ | ︙ | |||
85 86 87 88 89 90 91 92 | $session = null; if (!defined('Garradin\INSTALL_PROCESS')) { $session = Session::getInstance(); $this->assign('config', Config::getInstance()); } $this->assign('session', $session); | > > | | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | $session = null; if (!defined('Garradin\INSTALL_PROCESS')) { $session = Session::getInstance(); $this->assign('config', Config::getInstance()); } $is_logged = $session ? $session->isLogged() : null; $this->assign('session', $session); $this->assign('is_logged', $is_logged); $this->assign('logged_user', $is_logged ? $session->getUser() : null); $this->assign('session', $session); $this->assign('dialog', isset($_GET['_dialog'])); $this->assign('password_pattern', sprintf('.{%d,}', Session::MINIMUM_PASSWORD_LENGTH)); $this->assign('password_length', Session::MINIMUM_PASSWORD_LENGTH); $this->register_compile_function('continue', function (Smartyer $s, $pos, $block, $name, $raw_args) { |
︙ | ︙ | |||
110 111 112 113 114 115 116 | { return sprintf('use %s;', $raw_args); } }); $this->register_function('form_errors', [$this, 'formErrors']); $this->register_function('show_error', [$this, 'showError']); | < | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | { return sprintf('use %s;', $raw_args); } }); $this->register_function('form_errors', [$this, 'formErrors']); $this->register_function('show_error', [$this, 'showError']); $this->register_function('input', [$this, 'formInput']); $this->register_function('password_change', [$this, 'passwordChangeInput']); $this->register_function('custom_colors', [$this, 'customColors']); $this->register_function('plugin_url', ['Garradin\Utils', 'plugin_url']); $this->register_function('diff', [$this, 'diff']); $this->register_function('display_permissions', [$this, 'displayPermissions']); |
︙ | ︙ | |||
400 401 402 403 404 405 406 407 408 409 410 411 412 413 | if ($v = \DateTime::createFromFormat('!Y-m-d H:i:s', $current_value)) { $current_value = $v->format('H:i'); } elseif ($v = \DateTime::createFromFormat('!Y-m-d H:i', $current_value)) { $current_value = $v->format('H:i'); } } $attributes['id'] = 'f_' . str_replace(['[', ']'], '', $name); $attributes['name'] = $name; if (!isset($attributes['autocomplete']) && ($type == 'money' || $type == 'password')) { $attributes['autocomplete'] = 'off'; | > > > | 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | if ($v = \DateTime::createFromFormat('!Y-m-d H:i:s', $current_value)) { $current_value = $v->format('H:i'); } elseif ($v = \DateTime::createFromFormat('!Y-m-d H:i', $current_value)) { $current_value = $v->format('H:i'); } } elseif ($type == 'password') { $current_value = null; } $attributes['id'] = 'f_' . str_replace(['[', ']'], '', $name); $attributes['name'] = $name; if (!isset($attributes['autocomplete']) && ($type == 'money' || $type == 'password')) { $attributes['autocomplete'] = 'off'; |
︙ | ︙ | |||
555 556 557 558 559 560 561 562 563 564 565 566 567 568 | $value = isset($attributes['value']) ? '' : sprintf(' value="%s"', $this->escape($current_value)); $input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value); } if ($type == 'file') { $input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize())); } $input .= $suffix; // No label? then we only want the input without the widget if (empty($label)) { if (!array_key_exists('label', $params) && ($type == 'radio' || $type == 'checkbox')) { $input .= sprintf('<label for="%s"></label>', $attributes['id']); | > > > | 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 | $value = isset($attributes['value']) ? '' : sprintf(' value="%s"', $this->escape($current_value)); $input = sprintf('<input type="%s" %s %s />', $type, $attributes_string, $value); } if ($type == 'file') { $input .= sprintf('<input type="hidden" name="MAX_FILE_SIZE" value="%d" id="f_maxsize" />', Utils::return_bytes(Utils::getMaxUploadSize())); } elseif (!empty($copy)) { $input .= sprintf('<input type="button" onclick="var a = $(\'#f_%s\'); a.focus(); a.select(); document.execCommand(\'copy\'); this.value = \'Copié !\'; this.focus(); return false;" onblur="this.value = \'Copier\';" value="Copier" title="Copier dans le presse-papier" />', $params['name']); } $input .= $suffix; // No label? then we only want the input without the widget if (empty($label)) { if (!array_key_exists('label', $params) && ($type == 'radio' || $type == 'checkbox')) { $input .= sprintf('<label for="%s"></label>', $attributes['id']); |
︙ | ︙ | |||
579 580 581 582 583 584 585 | if (isset($help)) { $out .= sprintf(' <em class="help">(%s)</em>', $this->escape($help)); } $out .= '</dd>'; } else { | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 | if (isset($help)) { $out .= sprintf(' <em class="help">(%s)</em>', $this->escape($help)); } $out .= '</dd>'; } else { $out = sprintf('<dt>%s%s</dt><dd>%s</dd>', $label, $required_label, $input); if ($type == 'file' && empty($params['no_size_limit'])) { $out .= sprintf('<dd class="help"><small>Taille maximale : %s</small></dd>', Utils::format_bytes(Utils::getMaxUploadSize())); } if (isset($help)) { $out .= sprintf('<dd class="help">%s</dd>', $this->escape($help)); } } return $out; } protected function formatPhoneNumber($n) { if (empty($n)) { return ''; } $country = Config::getInstance()->get('country'); |
︙ | ︙ |
Modified src/include/lib/Garradin/Users/Session.php from [c4e089fae6] to [9aa42938e1].
︙ | ︙ | |||
247 248 249 250 251 252 253 | $qrcode = new QRCode($out['url']); $out['qrcode'] = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG()); return $out; } | | | > > | > > | < | | 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | $qrcode = new QRCode($out['url']); $out['qrcode'] = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG()); return $out; } public function recoverPasswordSend(int $id): void { $user = $this->fetchUserForPasswordRecovery($id); if (!$user) { throw new UserException('Aucun membre trouvé avec cette adresse e-mail, ou le membre trouvé n\'a pas le droit de se connecter.'); } if ($user->perm_connect == self::ACCESS_NONE) { throw new UserException('Ce membre n\'a pas le droit de se connecter.'); } if (!trim($user->email)) { throw new UserException('Ce membre n\'a pas d\'adresse e-mail renseignée dans son profil.'); } $query = $this->makePasswordRecoveryQuery($user); $message = "Bonjour,\n\nVous avez oublié votre mot de passe ? Pas de panique !\n\n"; $message.= "Il vous suffit de cliquer sur le lien ci-dessous pour modifier votre mot de passe.\n\n"; $message.= ADMIN_URL . 'password.php?c=' . $query; $message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé."; if ($user->pgp_key) { $content = Security::encryptWithPublicKey($user->pgp_key, $message); } Emails::queue(Emails::CONTEXT_SYSTEM, [$user->email => null], null, 'Mot de passe perdu ?', $message); } protected function fetchUserForPasswordRecovery(int $id): ?\stdClass { $db = DB::getInstance(); $id_field = DynamicFields::getLoginField(); $email_field = DynamicFields::getFirstEmailField(); // Fetch user, must have an email $sql = sprintf('SELECT u.id, u.%s AS email, u.password, u.pgp_key, c.perm_connect FROM users u INNER JOIN users_categories c ON c.id = u.id_category WHERE u.%s = ? COLLATE NOCASE AND u.%1$s IS NOT NULL LIMIT 1;', $db->quoteIdentifier($email_field), $db->quoteIdentifier($id_field)); |
︙ | ︙ | |||
306 307 308 309 310 311 312 313 314 315 316 317 318 319 | $hash = hash_hmac('sha256', $user->email . $user->id . $user->password . $expire, SECRET_KEY, true); $hash = substr(Security::base64_encode_url_safe($hash), 0, 16); return $hash; } protected function makePasswordRecoveryQuery(\stdClass $user): string { $id = base_convert($user->id, 10, 36); $expire = base_convert($expire, 10, 36); return sprintf('%s.%s.%s', $id, $expire, $hash); } /** * Check that the supplied query is valid, if so, return the user information | > > | 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | $hash = hash_hmac('sha256', $user->email . $user->id . $user->password . $expire, SECRET_KEY, true); $hash = substr(Security::base64_encode_url_safe($hash), 0, 16); return $hash; } protected function makePasswordRecoveryQuery(\stdClass $user): string { $expire = ceil((time() - strtotime('2017-01-01')) / 3600) + 1; $hash = $this->makePasswordRecoveryHash($user, $expire); $id = base_convert($user->id, 10, 36); $expire = base_convert($expire, 10, 36); return sprintf('%s.%s.%s', $id, $expire, $hash); } /** * Check that the supplied query is valid, if so, return the user information |
︙ | ︙ | |||
352 353 354 355 356 357 358 | } return $user; } public function recoverPasswordChange(string $query, string $password, string $password_confirm) { | | | 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | } return $user; } public function recoverPasswordChange(string $query, string $password, string $password_confirm) { $user = $this->checkRecoveryPasswordQuery($query); if (null === $user) { throw new UserException('Le code permettant de changer le mot de passe a expiré. Merci de bien vouloir recommencer la procédure.'); } $password = trim($password); $password_confirm = trim($password_confirm); |
︙ | ︙ | |||
376 377 378 379 380 381 382 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$user->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('users', ['password' => $password], 'id = :id', ['id' => (int)$user->id]); | | | 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | $message = "Bonjour,\n\nLe mot de passe de votre compte a bien été modifié.\n\n"; $message.= "Votre adresse email : ".$user->email."\n"; $message.= "La demande émanait de l'adresse IP : ".Utils::getIP()."\n\n"; $message.= "Si vous n'avez pas demandé à changer votre mot de passe, merci de nous le signaler."; DB::getInstance()->update('users', ['password' => $password], 'id = :id', ['id' => (int)$user->id]); return Emails::queue(Emails::CONTEXT_SYSTEM, [$user->email => null], null, 'Mot de passe changé', $message); } public function user(): ?User { return $this->getUser(); } |
︙ | ︙ |
Modified src/templates/login.tpl from [52da43c89e] to [ddc91ba971].
1 2 3 4 5 | {include file="admin/_head.tpl" title="Connexion"} {form_errors} {if $changed} | | | | | | | | | < < < < < | < | | < | | | > > > | | | | | | < | < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | {include file="admin/_head.tpl" title="Connexion"} {form_errors} {if $changed} <p class="block confirm"> Votre mot de passe a bien été modifié.<br /> Vous pouvez maintenant l'utiliser pour vous reconnecter. </p> {/if} <p class="block error" style="display: none;" id="old_browser"> Le navigateur que vous utilisez n'est pas supporté. Des fonctionnalités peuvent ne pas fonctionner.<br /> Merci d'utiliser un navigateur web moderne comme <a href="https://www.getfirefox.com/" target="_blank">Firefox</a> ou <a href="https://vivaldi.com/fr/" target="_blank">Vivaldi</a>. </p> <form method="post" action="{$self_url}"> <fieldset> <legend> {if $ssl_enabled} <span class="confirm">{icon shape="lock"} Connexion sécurisée</span> {else} <span class="alert">{icon shape="unlock"} Connexion non-sécurisée</span> {/if} </legend> <dl> {input type=$id_field.type label=$id_field.label required=true name="id"} {input type="password" name="password" label="Mot de passe" required=true} {input type="checkbox" name="permanent" value="1" label="Rester connecté⋅e" help="recommandé seulement sur ordinateur personnel"} </dl> </fieldset> <p class="submit"> {csrf_field key="login"} {button type="submit" name="login" label="Se connecter" shape="right" class="main"} {linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"} {linkbutton href="!password.php?new" label="Première connexion ?" shape="user"} </p> </form> {literal} <script type="text/javascript"> if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) { document.getElementById('old_browser').style.display = 'block'; } g.enhancePasswordField($('#f_passe')); </script> {/literal} {include file="admin/_foot.tpl"} |
Modified src/templates/password.tpl from [6ce594bca0] to [c6d2d188c0].
|
| | < | > | > | > | | | | | | | | | | > > > | > | | < | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | {include file="admin/_head.tpl" title=$title} {if $sent} <p class="block confirm"> {if $new} Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail pour choisir votre mot de passe. {else} Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail pour modifier votre mot de passe. {/if} </p> <p class="help"> Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables. </p> {else} {form_errors} <form method="post" action="{$self_url}"> <fieldset> <legend>{if $new}Envoyer un e-mail pour choisir son mot de passe{else}Envoyer un e-mail pour modifier son mot de passe{/if}</legend> <p class="help"> Inscrivez ici votre identifiant.<br/> {if $new} Vous recevrez un e-mail à l'adresse renseignée dans votre fiche membre, avec un lien vous permettant de créer votre mot de passe. {else} Nous vous enverrons un e-mail à l'adresse renseignée dans votre fiche membre, avec un lien vous permettant de modifier votre mot de passe. {/if} </p> <dl> {input type=$id_field.type label=$id_field.label required=true name="id"} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="recover" label="Envoyer" shape="right" class="main"} </p> </form> {/if} {include file="admin/_foot.tpl"} |
Modified src/templates/password_change.tpl from [4df344e2c8] to [73a04e08fa].
︙ | ︙ | |||
9 10 11 12 13 14 15 | <legend>Choisir un nouveau mot de passe</legend> <dl> {include file="users/_password_form.tpl" required=true} </dl> </fieldset> <p class="submit"> | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <legend>Choisir un nouveau mot de passe</legend> <dl> {include file="users/_password_form.tpl" required=true} </dl> </fieldset> <p class="submit"> {csrf_field key=$csrf_key} {button type="submit" name="change" label="Modifier mon mot de passe" shape="right" class="main"} </p> </form> {include file="admin/_foot.tpl"} |
Modified src/templates/users/_password_form.tpl from [2f1eb246b7] to [17aea97add].
︙ | ︙ | |||
9 10 11 12 13 14 15 | ?> <dd class="help"> Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. </dd> <dd class="help"> Pas d'idée ? Voici une suggestion choisie au hasard : | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ?> <dd class="help"> Astuce : un mot de passe de quatre mots choisis au hasard dans le dictionnaire est plus sûr et plus simple à retenir qu'un mot de passe composé de 10 lettres et chiffres. </dd> <dd class="help"> Pas d'idée ? Voici une suggestion choisie au hasard : {input type="text" readonly=true title="Cliquer pour utiliser cette suggestion comme mot de passe" default=$suggestion autocomplete="off" copy=true name="suggest"} </dd> {input type="password" name="password" required=$required label="Mot de passe" help="Minimum %d caractères"|args:$password_length autocomplete="off" minlength=$password_length} {input type="password" name="password_confirmed" required=$required label="Encore le mot de pase (vérification)" help="Minimum %d caractères"|args:$password_length autocomplete="off" minlength=$password_length} <script type="text/javascript" async="async"> {literal} g.script('scripts/password.js', () => { initPasswordField('f_suggest', 'f_password', 'f_password_confirmed'); }); {/literal} </script> |
Modified src/www/admin/login.php from [3f8f31fcb9] to [82663e1336].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?php namespace Garradin; use KD2\HTTP; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; // Relance session_start et renvoie une image de 1px transparente if (qg('keepSessionAlive') !== null) { $session->keepAlive(); header('Cache-Control: no-cache, must-revalidate'); | > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php namespace Garradin; use KD2\HTTP; use Garradin\Users\DynamicFields; use Garradin\Users\Session; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; $session = Session::getInstance(); // Relance session_start et renvoie une image de 1px transparente if (qg('keepSessionAlive') !== null) { $session->keepAlive(); header('Cache-Control: no-cache, must-revalidate'); |
︙ | ︙ | |||
27 28 29 30 31 32 33 | Utils::redirect(ADMIN_URL . ''); } $id_field = DynamicFields::get(DynamicFields::getLoginField()); $id_field_name = $id_field->label; $form->runIf('login', function () use ($id_field_name, $session) { | | | | > | < | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | Utils::redirect(ADMIN_URL . ''); } $id_field = DynamicFields::get(DynamicFields::getLoginField()); $id_field_name = $id_field->label; $form->runIf('login', function () use ($id_field_name, $session) { if (!trim((string) f('id'))) { throw new UserException(sprintf('L\'identifiant (%s) n\'a pas été renseigné.', $id_field_name)); } if (!trim((string) f('password'))) { throw new UserException('Le mot de passe n\'a pas été renseigné.'); } if (!$session->login(f('id'), f('password'), (bool) f('permanent'))) { throw new UserException(sprintf("Connexion impossible.\nVérifiez votre identifiant (%s) et votre mot de passe.", $id_field_name)); } }, 'login', ADMIN_URL); $ssl_enabled = HTTP::getScheme() == 'https'; $changed = qg('changed') !== null; $tpl->assign(compact('id_field', 'ssl_enabled', 'changed')); $tpl->display('login.tpl'); |
Modified src/www/admin/password.php from [cecdb57f0d] to [0bb839b213].
1 2 3 4 5 6 7 8 9 10 | <?php namespace Garradin; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; $session = Session::getInstance(); | > > > | | > | > | < < < < < < < | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <?php namespace Garradin; use Garradin\Users\DynamicFields; use Garradin\Users\Session; const LOGIN_PROCESS = true; require_once __DIR__ . '/_inc.php'; $session = Session::getInstance(); $form->runIf(qg('c') !== null, function () use ($session, $form, $tpl) { if (!$session->checkRecoveryPasswordQuery(qg('c'))) { throw new UserException('Le lien que vous avez suivi est invalide ou a expiré.'); } $csrf_key = 'password_change_' . md5(qg('c')); $form->runIf('change', function () use ($session) { $session->recoverPasswordChange(qg('c'), f('password'), f('password_confirmed')); }, $csrf_key, '!login.php?changed'); $tpl->assign(compact('csrf_key')); $tpl->display('password_change.tpl'); exit; }); $csrf_key = 'recover_password'; $new = qg('new') !== null; $form->runIf('recover', function () use ($session) { $session->recoverPasswordSend((int) f('id')); }, $csrf_key, '!password.php?sent' . ($new ? '&new' : '')); $sent = !$form->hasErrors() && null !== qg('sent'); $id_field = DynamicFields::get(DynamicFields::getLoginField()); $title = $new ? 'Première connexion ?' : 'Mot de passe perdu ?'; $tpl->assign(compact('id_field', 'sent', 'csrf_key', 'title', 'new')); $tpl->display('password.tpl'); |