Zapomenutá hesla

Rudolf Svátek 2016-07-22 15:50

Dnes si můžeš stáhnout soubor kapitola15.zip.

Ruku na srdeční sval - kdo kdy nezapomněl nějaké heslo. Nejsem výjimka. Dokonce jsem si kvůli tomu napsal aplikaci, do které si hesla skládám. Musím si totiž evidovat i hesla na FTP svých klientů, do administrací jejich redakčních systémů apod. Jestli ale zapomene heslo náš uživatel, 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 upravovat budeme presenter app\FrontModule\presenters\SignPresenter.php. Slušelo by se říct, že klidně můžeme používat několik způsobů přihlašování. Jeden pro administraci, další pro přihlášení uživatele na frontend třeba do uživatelské sekce, ale tady si vystačíme s jedním přihlášením.

Zapíšeme inject služeb formulářů - ty ještě neexistují, ale nějak se začít musí. Služby formulářů taky dodáme. V presenteru tedy přidej:

	/** @var Forms\SignRememberFormFactory @inject */
	public $signRememberFactory;

	/** @var Forms\NewPassFormFactory @inject */
	public $newPassFactory;

No a taky samozřejmě musíme vytvořit komponenty těch dvou nových formulářů:

	/**
	 * @return Nette\Application\UI\Form
	 */
	protected function createComponentSignRememberForm() {
		$form = $this->signRememberFactory->create();
		$form->onSuccess[] = function (Form $form) {
			$values = $form->getValues();
			/** @var UsersEntity $user */
			$user = NULL;
			if ($values->username > '')
				$user = $this->usersRepository->getOneWhere(['username' => $values->username]);

			if (is_null($user) && $values->email > '')
				$user = $this->usersRepository->getOneWhere(['email' => $values->email]);

			if (is_null($user)) {
				$this->flashMessage('Neznámý uživatel', 'error');
				$this->redirect('this');
			}

			$now = new DateTime();
			$validity = new DateTime('+1 day');
			$user->passwordHash(Passwords::hash($now . $user->email()));
			$user->passwordHashValidity($validity);
			$this->usersRepository->save($user);

			$message = new Message;

			$params = [
				'username' => $user->username(),
				'token' => $user->passwordHash(),
			];
			$latte = new Engine();
			UIMacros::install($latte->getCompiler());
			$message->setFrom('noreply@sandbox.loc')
					->addTo($user->email())
					->setSubject('Zapomenuté heslo')
					->setHtmlBody($latte->renderToString(__DIR__ . '/templates/Sign/rememberMail.latte', $params));

			$mailer = $this->setMailer();

			$mailer->send($message);

			$this->restoreRequest($this->backlink);
			$this->flashMessage('Zkontrolujte si prosím svou mailovou schránku', 'success');
			$this->redirect('Default:default');
		};
		return $form;
	}

	/**
	 * @return Form
	 */
	protected function createComponentNewPassForm() {
		$form = $this->signNewPassFactory->create();
		$form->onSuccess[] = function (Form $form) {
			$values = $form->getValues();
			/** @var UsersEntity $user */
			$user = $this->usersRepository->getOneWhere(
					[
						'username' => $values->username,
						'passwordHash' => $values->passwordHash,
					]
			);
			if (is_null($user)) {
				$this->presenter->flashMessage('Neznámý uživatel', 'error');
				$this->presenter->redirect('in');
			}
			$user->passwordHash('');
			$user->passwordHashValidity('');
			$user->password(Passwords::hash($values->password));
			$this->usersRepository->save($user);
			$this->presenter->flashMessage('Heslo bylo změněno', 'success');
			$this->redirect('in');
		};
		return $form;
	}

Rozhodl jsem se zpracovávat formuláře v presenteru, takže proto je tam to $form->onSuccess[]. Metoda createComponentSignRememberForm odesílá mail. Aby to šlo, tak musíme presenter naučit odesílat maily. Jde to buď přes sendmail, nebo SMTP mail. Když to budeš zkoušet na svém počítači, je možné, že sendmail nebudeš mít nakonfigurovaný. Použijeme tedy SMTP, ale ten také vyžaduje konfiguraci. Nicméně stačí vyplnit normálně přihlašovací údaje k tvému SMTP serveru. Je možné, že posílat mail budeme chtít na více místech. Takže konfigurace SMTP bude v BasePresenteru:

	/**
	 * @return SendmailMailer
	 */
	protected function setMailer() {
		/** @var SmtpMailer mailer */
		$mailer = new SmtpMailer([
			'host' => 'smtpServer',
			'username' => 'smtpUsername',
			'password' => 'smtpPassword',
			'secure' => 'smtpSecure',
		]);
		return $mailer;
	}

Samozřejmě, že údaje si vyplníš svoje.

Když uživatel zobrazí formulář, který po něm chce, aby zadal svůj email, na který mu přijde zpráva s dalším postupem, nic do něj předvyplňovat nemusíme. Jakmile ale už chce zadat nové heslo, musíme do skrytých polí formuláře doplnit ten dočasný hash, jeho username a časovou značku, do kdy může heslo změnit. Potřebujeme tedy metodu:

	/**
	 * @param $username
	 * @param $token
	 */
	public function renderNewPass($username, $token) {
		/** @var UsersEntity $user */
		$user = $this->usersRepository->getOneWhere(['username' => $username, 'passwordHash' => $token]);

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

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

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

Na řádku 7 se hledá uživatel podle svého username a ověřuje se, že právě on má to dočasné hashované heslo, čili token. Pokud se takový uživatel nenajde, nic se nebude dělat. Jestli se takový uživatel najde, potřebujeme ještě zjistit, jestli přišel včas. Pokud to nestihl, tak i když o heslo požádal a víme, že je to skutečně on, tak přesto mu heslo změnit nedovolíme. Navíc mu smažeme ten dočasný token i časovou značku a teď se to tváří, jako by o změnu hesla ani nepožádal.

Teprve pokud jsou všechny podmínky splněny, doplníme do formuláře do skrytých polí username, ten dočasný hash a časovou značku. Pak čekáme, co uživatel zadá za nové heslo.

Tak dál - vyrobíme ty 2 formuláře. První pro žádost o obnovení hesla a druhý pro zadání nového hesla. Formuláře pro přihlášení i registraci už leží v app\forms. Tak ty 2 nové tam přidáme.

SignRememberFormFactory.php:

<?php

namespace App\Forms;

use Nette\Application\UI\Form;

class SignRememberFormFactory
{

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

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

	/**
	 * @return 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::FILLED, TRUE)
				->addRule(Form::FILLED, 'Vložte buď email, nebo uživatelské jméno');

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

		return $form;
	}

}

SignNewPassFormFactory.php:

<?php

namespace App\Forms;

use Nette\Application\UI\Form;

class SignNewPassFormFactory
{

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

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

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

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

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

		return $form;
	}

}

Věřím, že už jsi tak daleko, že potřebné šablony vymyslíš, služby v configu taky založíš. Kdyby ne, můžeš si zase stáhnout celou aplikaci jak vypadá k dnešnímu dni. Já si trochu s těmi šablonami pohrál a udělal je pěkné. Teda aspoň mě se líbí :-).

Dnes si můžeš stáhnout soubor kapitola15.zip.

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

Foto galerie

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

Vývoj redakčního systému v PHP

Redakční systém RS::RS Následující kapitola

Administrace komentářů

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