Zapomenuté heslo pro přihlášení

Rudolf Svátek 2017-08-21 14:38

Píšeme aplikaci na ukládání hesel, takže asi si všechna hesla nepamatujeme. Průšvih je, když zapomeneme heslo k aplikaci pro hesla :-). No, on to zase takový průšvih není - můžeme zapomětlivému uživateli pomoci. Ale nemůžeme mu poslat heslo! Nemůžeme mu ho vlastně říct, protože v naší databázi uživatelů je uloženo třeba:

$2y$10$pRWlZ22kM3AQgoYAKHSxtOh2pkxL9xsd0R2Z/167conLdUfA0N7jO

To je hash hesla, které v čitelné podobě zní: "user123". Je ovšem zajímavé, že když stejné heslo zadám jinému uživateli, ten má hash jiný. Třeba:

$2y$10$9wMxZoHAaDGWCk3U6.SCYeFNHCay4.GshYeqEfRThl1N9mNwFfo0i

A aby toho nebylo málo, tak když stejnému uživateli v administraci zadám znovu stejné heslo a uložím záznam, hash se zase změní. Nette to dělá takto šikovně, aby se nestalo, že si někdo stáhne databázi s účty uživatelů a zkouší si měnit své heslo. Kdyby se pak jeho hash shodoval s nějakým jiným uživatelem, znal by jeho heslo. Takhle mají oba uživatelé sice stejné heslo, ale hash jiný :-)

Jenže co dělat, když uživatel vážně heslo nezná? Určitě si zakážeme jakákoli hesla posílat mailem, volat přes telefon, nebo psát na papírek. My to uděláme fikaně. A v několika krocích:

  1. nejdříve zapomětlivému uživateli zobrazíme formulář, ve kterém ho požádáme o jeho přihlašovací jméno, nebo emai. To by snad vědět měl. Pokud neví ani to, tak to má zatím fakt smůlu
  2. po odeslání formuláře zkusíme uživatele podle jeho username či mailu najít. pokud se to povede, pak mu vygenerujeme náhodný hash, třeba z kombinace aktuálního času a jeho mailu a takový hash uložíme k jeho záznamu v tabulce users
  3. také si uděláme časovou značku, která bude říkat, jak dlouho je žádost o změnu hesla platná. Třeba hodinu, dvě, nebo jeden den
  4. uživateli pošleme zprávu na jeho mail s dalšími pokyny. Budeme vlastně chtít, aby zašel na konkrétní stránku, kde se mu ukáže formulář s možností zadat nové heslo. V tom formuláři si přichystáme skrytá pole s jeho uživatelským jménem, emailem a tím dočasným hashem
  5. ve formuláři uvidí uživatel pouze jedno políčko pro nové heslo.
  6. po odeslání nového hesla se nejprve zkontroluje, zda:
    1. existuje uživatel s daným username
    2. zda má správný dočasný hash
    3. zda to stihnul před vypršením limitu
  7. pokud jsou podmínky splněny, heslo mu dovolíme uložit

Tím je bezpečnost zaručena - nejen že se v žádném okamžiku neposílá čitelné heslo, ale dokonce ani hash opravdového hesla. V žádném okamžiku neví nikdo jaké heslo používá, kromě samotného uživatele. Ani aplikace, ani administrátor.

Bude to vyžadovat celkem dost práce, ale s chutí do toho :-).

Tak nejdříve vytvoříme definici 2 formulářů - pro žádost o nové heslo a pro vložení nového hesla:

<?php

namespace App\Forms;

use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;

/**
 * Class SignRememberFormFactory
 *
 * @package App\Forms
 */
class SignRememberFormFactory
{
    use Nette\SmartObject;

    /** @var FormFactory */
    private $factory;

    /** @var User */
    private $user;

    /**
     * SignRememberFormFactory constructor.
     *
     * @param \App\Forms\FormFactory $factory
     * @param \Nette\Security\User   $user
     */
    public function __construct(FormFactory $factory, User $user)
    {
        $this->factory = $factory;
        $this->user = $user;
    }

    /**
     * Method create
     *
     * @return \Nette\Application\UI\Form
     */
    public function create()
    {
        $form = $this->factory->create();
        $form->addText('email', 'Váš email')
            ->addCondition(Form::FILLED)
            ->addRule(Form::EMAIL, 'Vložte validní email');

        $form->addText('username', 'Uživatelské jméno')
            ->addConditionOn($form['email'], Form::BLANK, true)
            ->addRule(Form::FILLED, 'Vložte buď email, nebo uživatelské jméno');

        $form->addSubmit('send', 'Odeslat');

        return $form;
    }
}

To je ta žádost o vygenerování tokenu. No a formulář pro vložení nového hesla:

<?php

namespace App\Forms;

use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;

class SignNewPassFormFactory
{

    use Nette\SmartObject;

    /** @var FormFactory */
    private $factory;

    /** @var User */
    private $user;

    public function __construct(FormFactory $factory, User $user)
    {
        $this->factory = $factory;
        $this->user = $user;
    }

    /**
     * @return Form
     */
    public function create()
    {
        $form = $this->factory->create();
        $form->addHidden('username');
        $form->addHidden('password_hash');
        $form->addHidden('password_hash_validity');

        $form->addPassword('password', 'Nové heslo')
            ->setRequired('Vyplňte své nové heslo');

        $form->addSubmit('send', 'Odeslat');

        return $form;
    }
}

Pak už stačí vytvořit presenter:

<?php

namespace App\Presenters;

use App\Forms;
use App\Model\Users\GetUserByUserNameOREmail;
use App\Model\Users\UpdateUser;
use Dibi\Connection;
use Dibi\DateTime;
use Latte\Engine;
use Nette\Application\UI\Form;
use Nette\Application\UI\Presenter;
use Nette\Bridges\ApplicationLatte\UIMacros;
use Nette\Mail\Message;
use Nette\Mail\SendmailMailer;
use Nette\Mail\SmtpMailer;
use Nette\Security\Passwords;

/**
 * Class SignPresenter
 *
 * @package App\Presenters
 */
class SignRememberPresenter extends Presenter
{
    /** @var Forms\SignRememberFormFactory @inject */
    public $signRememberFactory;

    /** @var Connection */
    private $db;

    /**
     * SignRememberPresenter constructor.
     *
     * @param Connection              $db
     */
    public function __construct(Connection $db)
    {
        parent::__construct();
        $this->db = $db;
    }

    /**
     * Method actionDefault
     */
    public function actionDefault()
    {
        $this->getTemplate()->setFile(__DIR__ . "/templates/remember.latte");
    }

    /**
     * Method createComponentSignRememberForm
     *
     * @return \Nette\Application\UI\Form
     */
    protected function createComponentSignRememberForm()
    {
        $form = $this->signRememberFactory->create();

        $form->onSuccess[] = function (Form $form) {
            $userForRememberPasswordModel = new GetUserByUserNameOREmail($this->db);
            $values = $form->getValues();
            $string = $values->email > "" ? $values->email : $values->username;
            $userForRememberPassword = $userForRememberPasswordModel->get($string);

            if ($userForRememberPassword === false) {
                $this->flashMessage('Neznámý uživatel', 'danger');
                $this->redirect('this');
            }

            $now = new DateTime();
            $validity = new DateTime('+1 hour');
            $userForRememberPassword->passwordHash = Passwords::hash($now . $userForRememberPassword->email);
            $userForRememberPassword->passwordHashValidity = $validity;
            $userForRememberPasswordsUpdateModel = new UpdateUser($this->db);
            $userForRememberPasswordsUpdateModel->update($userForRememberPassword);

            /** @var Message $message */
            $message = new Message();

            $uri = $this->getHttpRequest()->getUrl();

            $params = [
                'username' => $userForRememberPassword->username,
                'token' => $userForRememberPassword->passwordHash,
                'sitename' => $uri->host,
            ];
            $latte = new Engine();
            $latte->addProvider("uiControl", $this);
            $latte->addProvider("uiPresenter", $this);
            UIMacros::install($latte->getCompiler());
            $message->setFrom('hesla@svatkovi.net')
                ->addTo($userForRememberPassword->email)
                ->setSubject('Zapomenuté heslo')
                ->setHtmlBody($latte->renderToString(__DIR__ . '/templates/rememberMail.latte', $params));

            $mailer = $this->setMailer();
            $mailer->send($message);

            $this->flashMessage('Zkontrolujte si prosím svou mailovou schránku', 'success');
            $this->redirect('SignIn:');
        };

        return $form;
    }

    /**
     * @return mixed
     */
    public function formatLayoutTemplateFiles()
    {
        $originalLayoutFiles = parent::formatLayoutTemplateFiles();
        $layoutFiles = [];
        $layoutFiles[] = __DIR__ . '/templates/@layout.latte';
        $layoutFiles = array_merge($layoutFiles, $originalLayoutFiles);

        return $layoutFiles;
    }

    /**
     * Method setMailer
     *
     * @param int $useSendmail
     *
     * @return \Nette\Mail\SendmailMailer|\Nette\Mail\SmtpMailer
     */
    protected function setMailer($useSendmail = 1)
    {
        if (1 === $useSendmail) {
            /** @var SendmailMailer mailer */
            $mailer = new SendmailMailer();
        } else {
            /** @var SmtpMailer mailer */
            $mailer = new SmtpMailer(
                [
                    'host' => 'smtp.gmail.com',
                    'username' => 'email@gmail.com',
                    'password' => 'heslo',
                    'secure' => 'tls',
                    'port' => 587,
                ]
            );
        }

        return $mailer;
    }

}

Tohle už je trošku náročnější, takže malý popis:

Po odeslání se pokusíme najít uživatele podle zadaného username či emailu. Když se to nepovede, zobrazíme chybovou hlášku a končíme. Pokud ale uživatele najdeme, nastavíme mu validační hash a datum platnosti. Pak odešleme zprávu na nalezený email uživatele.

Aby to fungovalo, potřebujeme 2 šablony - pro zobrazení formuláře a pro ten odesílaný mail. Šablona app\presenters\Sign\templates\remember.latte:

{block content}

<div class="form-2 w600" n:if="$flashes">
  {snippet flash}
    <div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} fade in">
      <p>{$flash->message}</p>
    </div>
  {/snippet}
</div>
<section class="main">
	<form n:name=signRememberForm class="form-2 w600">
		<h1><span class="sign-up">Zapomenuté heslo</span></h1>
        <div><p>Zapoměli jste heslo? To se stane. Nemůžeme vám jej zaslat, protože dbáme na bezpečnost a heslo neznáme.
            Vyplňte níže jeden z údajů a my vám zašleme odkaz na vygenerování nového hesla.</p></div>
        <div><p>Po odeslání formuláře vám přijde zpráva s pokyny na emailovou adresu, kteru jste zadali při registraci, nebo která vám byla přidělena.</p></div>
		<p class="float">
			<label n:name=username><i class="fa fa-user"></i>Váš Login</label>
			<input type="text"  n:name=username placeholder="Login">
		</p>
		<p class="float">
			<label n:name=email><i class="fa fa-envelope"></i>Váš Email</label>
			<input type="text"  n:name=email placeholder="Email">
		</p>
		<p class="clearfix"> 
			<input type="submit" n:name=send value="Resetovat heslo">
		</p>
	</form>​​
</section>

{/block}

{block title}
  Zapomenuté heslo 
{/block}

A ještě šablona rememberMail.latte ve stejné složce:

<html>
    <head>
        <title>Zapomenuté heslo</title>
    </head>

    <body>
      <p>Někdo (doufáme, že vy) požádal o změnu přihlašovacího hesla na webu {$sitename}. Pokud si opravdu přejete
          změnit heslo, klikněte na odkaz níže a následujte instrukce.</p>
      <p><a n:href="//SignNewPass:default, username => $username, token => $token">Vygenerovat nové heslo</a></p>
    </body>
</html>

No a teď je na řadě uživatel. Měl by jít do své emailové schránky, kde má mail s odkazem. Jakmile na něj klikne, přijde ke slovu SignNewPassPresenter:

<?php

namespace App\Presenters;

use App\Forms;
use App\Model\Users\GetUserForNewPassword;
use App\Model\Users\GetUserByUserNameOREmail;
use App\Model\Users\UpdateUser;
use Dibi\Connection;
use Dibi\DateTime;
use Nette\Application\UI\Form;
use Nette\Application\UI\Presenter;
use Nette\Security\Passwords;

/**
 * Class SignPresenter
 *
 * @package App\Presenters
 */
class SignNewPassPresenter extends Presenter
{
    /** @var Forms\SignNewPassFormFactory @inject */
    public $signNewPassFactory;

    /** @var Connection */
    private $db;

    /**
     * SignPresenter constructor.
     *
     * @param Connection $db
     */
    public function __construct(Connection $db)
    {
        parent::__construct();
        $this->db = $db;
    }

    /**
     * Method actionDefault
     *
     * @param string $username
     * @param string $token
     */
    public function actionDefault($username, $token)
    {
        $this->getTemplate()->setFile(__DIR__ . "/templates/newPass.latte");
        $getUserForNewPasswordModel = new GetUserForNewPassword($this->db);
        $userForNewPassword = $getUserForNewPasswordModel->get($username, $token);

        if ($userForNewPassword === false) {
            $this->flashMessage('Nepodařilo se najít uživatelský účet pro obnovu hesla', 'error');
            $this->redirect('SignIn:');
        }

        $now = new DateTime();
        if ($userForNewPassword->passwordHashValidity < $now) {
            $userForNewPassword->passwordHash = '';
            $userForNewPassword->passwordHashValidity = '';
            $userForNewPasswordsUpdateModel = new UpdateUser($this->db);
            $userForNewPasswordsUpdateModel->update($userForNewPassword);
            $this->presenter->flashMessage('Vypršela platnost tokenu. Požádejte prosím o nové heslo znovu. Platnost žádosti je 1 hodina.', 'error');
            $this->presenter->redirect('remember');
        }

        /** @var Form $form */
        $form = $this['newPassForm'];
        if (!$form->isSubmitted()) {
            $row = [
                'username' => $userForNewPassword->username,
                'passwordHash' => $userForNewPassword->passwordHash,
                'passwordHashValidity' => $userForNewPassword->passwordHashValidity,
            ];
            $form->setDefaults($row);
        }
    }

    /**
     * Method createComponentNewPassForm
     *
     * @return \Nette\Application\UI\Form
     */
    protected function createComponentNewPassForm()
    {
        $form = $this->signNewPassFactory->create();

        $form->onSuccess[] = function (Form $form) {
            $values = $form->getValues();
            $userForRememberPasswordModel = new GetUserByUserNameOREmail($this->db);
            $userForRememberPassword = $userForRememberPasswordModel->get($values->username);
            $userForRememberPassword->passwordHash = '';
            $userForRememberPassword->passwordHashValidity = null;
            $userForRememberPassword->password = Passwords::hash($values->password);
            $userUpdateModel = new UpdateUser($this->db);
            $userUpdateModel->update($userForRememberPassword);
            $this->flashMessage('Heslo bylo změněno', 'success');
            $this->redirect('Passwords:');
        };

        return $form;
    }

    /**
     * @return mixed
     */
    public function formatLayoutTemplateFiles()
    {
        $originalLayoutFiles = parent::formatLayoutTemplateFiles();
        $layoutFiles = [];
        $layoutFiles[] = __DIR__ . '/templates/@layout.latte';
        $layoutFiles = array_merge($layoutFiles, $originalLayoutFiles);

        return $layoutFiles;
    }

}

No a šablona pro vložení nového hesla:

{block content}
	<div class="form-2" n:if="$flashes">
		{snippet flash}
			<div n:foreach="$flashes as $flash" class="alert alert-{$flash->type} fade in">
				<p>{$flash->message}</p>
			</div>
		{/snippet}
	</div>

	<section class="main">
		<form n:name=newPassForm class="form-2">
			<h1><span class="log-in">Nové heslo</span></h1>
			<ul class="errors" n:if="$form->hasErrors()">
				<li n:foreach="$form->errors as $error">{$error}</li>
			</ul>
			<input type="hidden" n:name=username>
			<input type="hidden" n:name=password_hash>
			<input type="hidden" n:name=password_hash_validity>
			<p>
				<label n:name=password><i class="fa fa-lock"></i>Nové heslo</label>
				<input type="password" tabindex="2" n:name=password placeholder="Heslo" class="showpassword">
				<label for="showPassword"><input id="showPassword" tabindex="4" class="showpasswordcheckbox"
				                                 type="checkbox">Zobrazit heslo</label>
			</p>
			<p class="clearfix">
				<input type="submit" n:name=send value=Změnit heslo">
			</p>
		</form>
		​​
	</section>
	<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
	<script type="text/javascript">
		$(function () {
			$(".showpassword").each(function (index, input) {
				var $input = $(input);
				$("#showPassword").click(function () {
					var change = $(this).is(":checked") ? "text" : "password";
					var rep = $("<input placeholder='Heslo' type='" + change + "' />")
							.attr("id", $input.attr("id"))
							.attr("name", $input.attr("name"))
							.attr('class', $input.attr('class'))
							.val($input.val())
							.insertBefore($input);
					$input.remove();
					$input = rep;
				})
			});
			$('#showPassword').click(function () {
				if ($("#showPassword").is(":checked")) {
					$('.icon-lock').addClass('icon-unlock');
					$('.icon-unlock').removeClass('icon-lock');
				} else {
					$('.icon-unlock').addClass('icon-lock');
					$('.icon-lock').removeClass('icon-unlock');
				}
			});
		});
	</script>
{/block}

{block title}
	Nové heslo
{/block}

Nesmíme zapomenout upravit config.form.neon:

services:
    - App\Forms\FormFactory
    - App\Forms\SignInFormFactory
    - App\Forms\SignRememberFormFactory
    - App\Forms\SignNewPassFormFactory

A také je už čas upravit v šabloně app\presenters\Sign\templates\in.latte ten odkaz na žádost o vygenerování hesla. Měl by tam být zase:

<a n:href="SignRemember:" class="btn btn-link btn-sm" role="button"><i class="fa fa-mail-reply-all"></i>
                    Zapomenuté heslo</a>

Teď je tedy možné z dialogu pro přihlášení kliknout na zapomenuté heslo a projít si celou proceduru.

A to je vše přátelé.

Redakční systém RS::RS Předchozí kapitola

Přihlašování do aplikace

Redakční systém RS::RS Celý seriál

Aplikace na správu hesel

 

Komentáře (0)

Přidej svůj komentář

O mně

Jmenuji se Rudolf Svátek. Jsem lektor výpočetní techniky a PHP programátor. Stavím firemní stránky a eshopy. Aby se mi to dělalo pohodlně, vytvořil jsem redakční systém RS::RS, který ti tu nabízím k použití.

Rychlý kontakt na mně

  • Rudolf Svátek
  • Telefon:
    +420 777 828 353
  • Email:
  • Adresa:
    Josefa Hory 1097/5
    736 01 Havířov
    ČR



Tyto stránky používají Cookies. Používáním stránek s tím souhlasíte Další informace