Безопасная авторизация с PHPixie 3

от автора

image
Сегодня вышел самый долгожданный компонент PHPixie 3 — Auth для авторизации пользователей. Авторизация это наиболее критическая часть любого приложения, сделать ее правильно трудно, а ошибки могут скомпрометировать множество пользователей, особенно если речь идет об оупенсорсе. Использование устарелых hash-функций, криптографически небезопасных генераторов случайных чисел, неправильная работа с кукисами встречаются слишком часто. Я уже когда-то писал о старой уязвимости в Laravel, которую кстати полностью так не исправили. Поэтому в PHPixie Auth я очень внимательно отнёсся к аутентификации, особенно к долгим сессиям и кукисам.

Кстати в конце статьи у меня для вас есть очень радостная новость (спойлер: PHPixie теперь член PHP-FIG)

Что делает PHPixie Auth безопасным:

  • использование password_hash() из PHP7, и пакета компатибильности для PHP5
  • аналогично с криптографически безопасным random_bytes()
  • следование защищенному методу работы с кукисами из jaspan.com/improved_persistent_login_cookie_best_practice

Последний пункт наиболее интересный и среди PHP фреймворков не имеет аналогов из коробки. Суть заключается в отдельной таблице для хранения токенов логина.

  1. При логине создается пара случайных строк: идентификатор серии и пароль, которые отдаются пользователю в форме куки
  2. Создается хеш серии с паролем и записывается в базу вместе с идентификатором юзера и сроком годности
  3. При повторном обращении на сайт хеш с куки сравнивается с хешем в базе, и если они совпадают то происходит логин, старый токен удаляется и пользователю создается новый, но с той же серией
  4. Если хеш не совпал, значит куки кто-то украл или пробует подобрать. В таком случае удаляются все токены с той же серией

Такой подход позволяет пользователю быть одновременно залогиненным на нескольких устройствах (одно устройство — одна серия). Например Laravel просто сохраняет токен в таблице пользователей, и как результат у пользователя токен может быть только один на все устройства.

Конфигурация

Как и другие компоненты PHPixie, Auth можно использовать и без фреймворка, но для простоты изложения я опишу как раз этот случай.

Во-первых вам понадобится репозиторий пользователей, каждый бандл может предоставлять свои репозитории, из которых мы выберем нужные уже в конфиг файле. Если вы используете ORM для работы с пользователями, то с Auth поставляется готовый враппер:

namespace Project\App\ORMWrappers\User;  // Враппер репозитория class Repository extends \PHPixie\AuthORM\Repositories\Type\Login {     //Есть поддержка логина по нескольким полям     // например по юзернейму и емейлу     protected function loginFields()     {          return array('username', 'email');     } } 
namespace Project\App\ORMWrappers\User;  // Враппер сущности class User extends \PHPixie\AuthORM\Repositories\Type\Login\User {     // указываем поле с хешем пароля     protected function passwordHash()     {          return $this->password;     } } 

Не забываем зарегистрировать их в ORMWrappers.php

namespace Project\App;  class ORMWrappers extends \PHPixie\ORM\Wrappers\Implementation {     protected $databaseEntities = array('user');     protected $databaseRepositories = array('user');      public function userEntity($entity)     {         return new ORMWrappers\User\Entity($entity);     }          public function userRepisitory($repository)     {         return new ORMWrappers\User\Repository($repository);     } } 

Теперь зарегистрируем этот репозиторий в бандле:

namespace Project\App;  class AuthRepositories extends \PHPixie\Auth\Repositories\Registry\Builder {     protected $builder;      public function __construct($builder)     {         $this->builder = $builder;     }      protected function buildUserRepository()     {         $orm = $this->builder->components()->orm();         return $orm->repository('user');     } } 
namespace Project\App;  class Builder extends \PHPixie\DefaultBundle\Builder {     protected function buildAuthRepositories()     {         return new AuthRepositories($this);     } } 

Также необходимо создать таблицу для хранения токенов (при использовании MongoDB все будет работать сразу):

 CREATE TABLE `tokens` (   `series` varchar(50) NOT NULL,   `userId` int(11) DEFAULT NULL,   `challenge` varchar(50) DEFAULT NULL,   `expires` bigint(20) DEFAULT NULL,   PRIMARY KEY (`series`) ) 

Теперь сам конфиг файл. Самый популярный подход будет выглядеть вот так:

// /assets/auth.php  return array(     'domains' => array(         'default' => array(              // репозиторий user из бандла app             'repository' => 'app.user',             'providers'  => array(                  // включаем поддержку сессий                 'session' => array(                     'type' => 'http.session'                 ),                  // поддержка кукисов (для "remember me")                 'cookie' => array(                     'type' => 'http.cookie',                                          // при логине сказать провайдеру session                     // чтобы тот запомнил юзера                     'persistProviders' => array('session'),                                          // где сохранять токены                     'tokens' => array(                         'storage' => array(                             'type'            => 'database',                             'table'           => 'tokens',                             'defaultLifetime' => 3600*24*14 // две недели                         )                     )                 ),                                  // поддержка логина паролем                 'password' => array(                     'type' => 'login.password',                                          // запомнить пользователя в сессии.                     // заметьте что в этом массиве нет 'cookies'                     // ведь мы будем делать "remember me" логин                     // не всегда, а только когда юзер сам попросит                     'persistProviders' => array('session')                 )             )         ) ); 

Все провайдеры независимы один от другого, что позволяет легко изменить функционал. Например если вы хотите использовать только куки, без сессий, и при этом отключить обновление токена на каждом запросе, то можете сделать вот так:

// /assets/auth.php  return array(     'domains' => array(         'default' => array(                'cookie' => array(                     'type' => 'http.cookie',                      // где сохранять токены                     'tokens' => array(                         'storage' => array(                             'type'                 => 'database',                             'table'                => 'tokens',                             'defaultLifetime' => 3600*24*14,                             'refresh'             => false                         )                     )                 ),                                  'password' => array(                     'type' => 'login.password',                     'persistProviders' => array('cookie')                 )             )         ) ); 
Использование

Cоздадим простенький процессор, чтобы попробовать как это все вместе работает:

namespace Project\App\HTTPProcessors;  class Auth extends \PHPixie\DefaultBundle\Processor\HTTP\Actions {     protected $builder;      public function __construct($builder)     {         $this->builder = $builder;     }      // Смотрим залогинен ли пользователь в домене     public function defaultAction($request)     {         $user = $this->domain()->user();          return $user ? $user->username : 'not logged';     }          // екшн для добавления пользователя в базу     public function addAction($request)     {         $query = $request->query();         $username = $query->get('username');         $password = $query->get('password');          $orm = $this->builder->components()->orm();         $provider = $this->domain()->provider('password');          $user = $orm->createEntity('user');          $user->username     = $username;          // хешыруем пароль используя провайдер         $user->passwordHash = $provider->hash($password);          $user->save();          return 'added';     }          // Логиним пользователя по паролю     public function loginAction($request)     {         $query = $request->query();         $username = $query->get('username');         $password = $query->get('password');          $provider = $this->domain()->provider('password');          $user = $provider->login($username, $password);                  if($user) {               $provider = $this->domain()->provider('password');         }         return $user ? 'success' : 'wrong password';     }          // логаут     public function logoutAction($request)     {         $this->domain()->forgetUser();         return 'logged out';     }           protected function domain()     {         $auth = $this->builder->components()->auth();         return $auth->domain();     } } 

Теперь заходим по урлах и смотрим результат:

  1. /auth — пользователь не залогинен
  2. /auth/add?username=jigpuzzled&password=5 — создаем пользователя
  3. /auth/login?username=jigpuzzled&password=5 — логинимся
  4. /auth — проверяем логин
  5. /auth/logout — логаут

Вы уже заметили что как и в конфиг файле, так и в процессоре встречается понятие домена. Домен это отдельный инстанс аутентификации со своим репозиторием и провайдерами. В большинстве случаев у вас будет только один домен. Несколько доменов понадобятся когда у вас будут отдельные секции сайта с разными логинами, например фронтенд позволяет пользователям зайти на сайт через логин фейсбука, а вот бекенд только через логин и пароль.

Свои провайдеры

Со временем вам наверняка понадобиться добавить свой провайдер логина, например для авторизации через социальные сети. Для этого вам понадобится сделать свой строитель провайдеров ( например по аналогии с Login) и зарегистрировать его как расширение. Для подробного описания я напишу отдельную статью но этих двух ссылок должно быть достаточно для начала.

PHPixie теперь член в PHP-FIG

Спасибо SamDark PHPixie уже от завтра будет членом PHP-FIG! Полный тред голосования можно увидеть тут. И еще огромное спасибо всем пользователям фреймворка, так как именно популярность и количество загрузок один из главных критериев отбора ^__^

ссылка на оригинал статьи http://habrahabr.ru/post/267177/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *