Overview
Comment:Ré-écriture récupération de mot de passe : ne plus utiliser de session mais un hash HMAC limité en durée (1 heure mini.)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 0077fd0f9a624b89241fc239694261a19988f0e0
User & Date: bohwaz on 2017-08-04 01:39:22
Other Links: branch diff | manifest | tags
Context
2017-08-04
05:28
Dans une asso, certaines dates du wiki étaient encore au format unix timestamp, corrigeons avant de migrer check-in: dc3f733f4e user: bohwaz tags: dev
01:39
Ré-écriture récupération de mot de passe : ne plus utiliser de session mais un hash HMAC limité en durée (1 heure mini.) check-in: 0077fd0f9a user: bohwaz tags: dev
2017-08-03
05:39
Modernisation cotisations check-in: 4e287c6f32 user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Membres/Session.php from [c7ce83b00e] to [ba6c3b5d63].

1
2
3
4
5
6
7
8
9


10
11
12
13
14
15
16
17
18
19
20
<?php

namespace Garradin\Membres;

use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\Membres;
use Garradin\UserException;



use \KD2\Security;
use \KD2\Security_OTP;
use \KD2\QRCode;

class Session
{
	const HASH_ALGO = 'sha256';
	const REQUIRE_OTP = 'otp';

	protected $cookie;









>
>

|
|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Garradin\Membres;

use Garradin\Config;
use Garradin\DB;
use Garradin\Utils;
use Garradin\Membres;
use Garradin\UserException;
use const Garradin\SECRET_KEY;
use const Garradin\WWW_URL;

use KD2\Security;
use KD2\Security_OTP;
use KD2\QRCode;

class Session
{
	const HASH_ALGO = 'sha256';
	const REQUIRE_OTP = 'otp';

	protected $cookie;
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

300













301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
	static public function recoverPasswordCheck($id)
	{
		$db = DB::getInstance();
		$config = Config::getInstance();

		$champ_id = $config->get('champ_identifiant');

		$membre = $db->first('SELECT id, email FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', trim($id));

		if (!$membre || trim($membre['email']) == '')
		{
			return false;
		}



		self::start(true);

		$hash = sha1($membre['email'] . $membre['id'] . 'recover' . ROOT . time());
		$_SESSION['recover_password'] = [
			'id' => (int) $membre['id'],

			'email' => $membre['email'],
			'hash' => $hash
		];

		$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 recevoir un nouveau mot de passe.\n\n";
		$message.= WWW_URL . 'admin/password.php?c=' . substr($hash, -10);
		$message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";

		return Utils::mail($membre['email'], '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message);

	}

	static public function recoverPasswordConfirm($hash)
	{
		self::start();


		if (empty($_SESSION['recover_password']['hash']))
			return false;

		if (substr($_SESSION['recover_password']['hash'], -10) != $hash)
			return false;

		$config = Config::getInstance();
		$db = DB::getInstance();



		$password = Utils::suggestPassword();






		$dest = $_SESSION['recover_password']['email'];

		$id = (int)$_SESSION['recover_password']['id'];















		$message = "Bonjour,\n\nVous avez demandé un nouveau mot de passe pour votre compte.\n\n";
		$message.= "Votre adresse email : ".$dest."\n";
		$message.= "Votre nouveau mot de passe : ".$password."\n\n";
		$message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";

		$password = Membres::hashPassword($password);

		$db->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$id]);

		return Utils::mail($dest, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message);
	}

	public function __construct()
	{
		if (empty($_SESSION['user']) && defined('\Garradin\LOCAL_LOGIN')
			&& is_int(\Garradin\LOCAL_LOGIN) && \Garradin\LOCAL_LOGIN > 0)
		{







|

|




>
>
|
>
|
|
|
>
|
|
<



|


|
>


|

<
>
|
<

|
|
|




>
>
|
>

>
>
>
>
|
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>

|







|







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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
	static public function recoverPasswordCheck($id)
	{
		$db = DB::getInstance();
		$config = Config::getInstance();

		$champ_id = $config->get('champ_identifiant');

		$membre = $db->first('SELECT id, email, passe FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', trim($id));

		if (!$membre || trim($membre->email) == '')
		{
			return false;
		}

		// valide pour 1 heure minimum
		$expire = ceil((time() - strtotime('2017-01-01')) / 3600) + 1;

		$hash = hash_hmac('sha256', $membre->email . $membre->id . $membre->passe . $expire, SECRET_KEY, true);
		$hash = substr(Security::base64_encode_url_safe($hash), 0, 16);
		
		$id = base_convert($membre->id, 10, 36);
		$expire = base_convert($expire, 10, 36);

		$query = sprintf('%s.%s.%s', $id, $expire, $hash);


		$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 recevoir un nouveau mot de passe.\n\n";
		$message.= WWW_URL . 'admin/password.php?c=' . $query;
		$message.= "\n\nSi vous n'avez pas demandé à recevoir ce message, ignorez-le, votre mot de passe restera inchangé.";

		Utils::mail($membre->email, '['.$config->get('nom_asso').'] Mot de passe perdu ?', $message);
		return true;
	}

	static public function recoverPasswordConfirm($code)
	{

		if (substr_count($code, '.') !== 2)
		{

			return false;
		}

		list($id, $expire, $email_hash) = explode('.', $code);

		$config = Config::getInstance();
		$db = DB::getInstance();

		$id = base_convert($id, 36, 10);
		$expire = base_convert($expire, 36, 10);

		$expire_timestamp = ($expire * 3600) + strtotime('2017-01-01');

		if (time() / 3600 > $expire_timestamp)
		{
			return false;
		}

		$membre = $db->first('SELECT id, email, passe FROM membres WHERE id = ? LIMIT 1;', (int)$id);

		if (!$membre || trim($membre->email) == '')
		{
			return false;
		}

		$hash = hash_hmac('sha256', $membre->email . $membre->id . $membre->passe . $expire, SECRET_KEY, true);
		$hash = substr(Security::base64_encode_url_safe($hash), 0, 16);

		if (!hash_equals($hash, $email_hash))
		{
			return false;
		}

		$password = Utils::suggestPassword();

		$message = "Bonjour,\n\nVous avez demandé un nouveau mot de passe pour votre compte.\n\n";
		$message.= "Votre adresse email : ".$membre->email."\n";
		$message.= "Votre nouveau mot de passe : ".$password."\n\n";
		$message.= "Si vous n'avez pas demandé à recevoir ce message, merci de nous le signaler.";

		$password = Membres::hashPassword($password);

		$db->update('membres', ['passe' => $password], 'id = :id', ['id' => (int)$id]);

		return Utils::mail($membre->email, '['.$config->get('nom_asso').'] Nouveau mot de passe', $message);
	}

	public function __construct()
	{
		if (empty($_SESSION['user']) && defined('\Garradin\LOCAL_LOGIN')
			&& is_int(\Garradin\LOCAL_LOGIN) && \Garradin\LOCAL_LOGIN > 0)
		{

Modified src/include/lib/Garradin/Utils.php from [04f73b6305] to [b7e5ac2f67].

433
434
435
436
437
438
439
440



441
442
443
444
445


446



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474

        $subject = '=?UTF-8?B?'.base64_encode($subject).'?=';

        if (is_array($to))
        {
            foreach ($to as $t)
            {
                return self::_sendMail($t, $suject, $content, $headers);



            }
        }
        else
        {
            return self::_sendMail($to, $subject, $content, $headers);


        }



    }

    static protected function _sendMail($to, $subject, $content, $headers)
    {
        if (SMTP_HOST)
        {
            $const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
            
            if (!defined($const))
            {
                throw new \LogicException('Configuration: SMTP_SECURITY n\'a pas une valeur reconnue. Valeurs acceptées: STARTTLS, SSL, NONE.');
            }

            $secure = constant($const);

            $smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
            return $smtp->send($to, $subject, $content, $headers);
        }
        else
        {
            return mail($to, $subject, $content, $headers);
        }
    }

    static public function clearCaches($path = false)
    {
        if (!$path)
        {







|
>
>
>




|
>
>
|
>
>
>




















|







433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482

        $subject = '=?UTF-8?B?'.base64_encode($subject).'?=';

        if (is_array($to))
        {
            foreach ($to as $t)
            {
                if (!self::_sendMail($t, $suject, $content, $headers))
                {
                    throw new \RuntimeException('Impossible d\'envoyer l\'email');
                }
            }
        }
        else
        {
            if (!self::_sendMail($to, $subject, $content, $headers))
            {
                throw new \RuntimeException('Impossible d\'envoyer l\'email');
            }
        }

        return true;
    }

    static protected function _sendMail($to, $subject, $content, $headers)
    {
        if (SMTP_HOST)
        {
            $const = '\KD2\SMTP::' . strtoupper(SMTP_SECURITY);
            
            if (!defined($const))
            {
                throw new \LogicException('Configuration: SMTP_SECURITY n\'a pas une valeur reconnue. Valeurs acceptées: STARTTLS, SSL, NONE.');
            }

            $secure = constant($const);

            $smtp = new SMTP(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, $secure);
            return $smtp->send($to, $subject, $content, $headers);
        }
        else
        {
            return mail('bohwaz', $subject, $content, $headers);
        }
    }

    static public function clearCaches($path = false)
    {
        if (!$path)
        {

Modified src/templates/admin/password.tpl from [35d28557ff] to [1feb370da9].

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
{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"}


{if !empty($sent)}
    <p class="confirm">
        Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail
        pour recevoir un nouveau mot de passe.
    </p>
    <p class="alert">
        <strong>Ne fermez pas cette fenêtre tant que vous n'avez pas cliqué sur le lien.</strong>
        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>
{elseif !empty($new_sent)}
    <p class="confirm">
        <strong>Un e-mail contenant votre nouveau mot de passe vous a été envoyé.</strong>
        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>
    <p><a href="{$www_url}admin/login.php">Connexion &rarr;</a></p>
{else}

    {if $error}
        <p class="error">
            {if $error == 'OTHER'}
                Une erreur est survenue, merci de réessayer.
            {else}
                Membre inconnu ou ne disposant pas d'adresse e-mail. Si vous êtes membre, contactez un responsable pour
                obtenir un mot de passe.
            {/if}
        </p>
    {/if}

    <form method="post" action="{$self_url}">

        <fieldset>
            <legend>Recevoir un e-mail avec un nouveau mot de passe</legend>
            <p class="help">
                Inscrivez ici votre {$champ.title}.
                Nous vous enverrons un message vous indiquant un lien permettant de recevoir un
                nouveau mot de passe.

>







<










|
<
<
<
<
<
<
<
<
<

|







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
{include file="admin/_head.tpl" title="Mot de passe oublié ou pas de mot de passe ?"}


{if !empty($sent)}
    <p class="confirm">
        Un e-mail vous a été envoyé, cliquez sur le lien dans cet e-mail
        pour recevoir un nouveau mot de passe.
    </p>
    <p class="alert">

        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>
{elseif !empty($new_sent)}
    <p class="confirm">
        <strong>Un e-mail contenant votre nouveau mot de passe vous a été envoyé.</strong>
        Si le message n'apparaît pas dans les prochaines minutes, vérifiez le dossier Spam ou Indésirables.
    </p>
    <p><a href="{$www_url}admin/login.php">Connexion &rarr;</a></p>
{else}

    {form_errors}










    <form method="post" action="{$admin_url}password.php">

        <fieldset>
            <legend>Recevoir un e-mail avec un nouveau mot de passe</legend>
            <p class="help">
                Inscrivez ici votre {$champ.title}.
                Nous vous enverrons un message vous indiquant un lien permettant de recevoir un
                nouveau mot de passe.

Modified src/www/admin/password.php from [8205464eee] to [26d86ab12a].

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
<?php

namespace Garradin;



const LOGIN_PROCESS = true;

require_once __DIR__ . '/_inc.php';

$error = false;

if (trim(qg('c')))
{
    if ($membres->recoverPasswordConfirm(qg('c')))
    {
        Utils::redirect('/admin/password.php?new_sent');
    }

    $error = 'EXPIRED';
}
elseif (f('recover'))
{
    $form->check('recoverPassword', [
        'id' => 'required'
    ]);

    if (!$form->hasErrors())
    {
        if (trim(f('id')) && $membres->recoverPasswordCheck(f('id')))
        {
            Utils::redirect('/admin/password.php?sent');
        }

        $form->addError('Ce membre n\'a pas d\'adresse email enregistrée ou n\'a pas le droit de se connecter.');
    }
}

>

>
>









|




|









|







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
<?php

namespace Garradin;

use Garradin\Membres\Session;

const LOGIN_PROCESS = true;

require_once __DIR__ . '/_inc.php';

$error = false;

if (trim(qg('c')))
{
    if (Session::recoverPasswordConfirm(qg('c')))
    {
        Utils::redirect('/admin/password.php?new_sent');
    }

    $form->addError('Le lien que vous avez suivi est invalide ou a expiré.');
}
elseif (f('recover'))
{
    $form->check('recoverPassword', [
        'id' => 'required'
    ]);

    if (!$form->hasErrors())
    {
        if (trim(f('id')) && Session::recoverPasswordCheck(f('id')))
        {
            Utils::redirect('/admin/password.php?sent');
        }

        $form->addError('Ce membre n\'a pas d\'adresse email enregistrée ou n\'a pas le droit de se connecter.');
    }
}