Аутентификация OAuth2 и токены JWT

После формы регистрации осталось добавить вход в кабинет и восстановление пароля. Для этого перед программированием нам нужно определиться, как мы будем осуществлять аутентификацию в приложении из JS-фронтенда и API.

Как раз сегодня рассмотрим практики аутентификации по токенам при работе с API и сравним классические подходы с использованием токенов в формате JWT.

И на слайдах смоделируем по шагам процесс получения и обновления токенов по универсальной спецификации OAuth2, чтобы с нашим API могли работать сторонние клиенты:

  • 00:02:39 - Basic Authentication
  • 00:04:40 - Использование токенов
  • 00:06:59 - Токены в распределённых системах
  • 00:08:28 - Хранение данных в токене
  • 00:11:06 - Инвалидация токена
  • 00:12:34 - Механизм обновления
  • 00:15:07 - JSON Web Token
  • 00:18:22 - Пакет lcobucci/jwt
  • 00:19:30 - Процесс аутентификации
  • 00:20:03 - Вход через сторонние сервисы
  • 00:24:07 - Использование одноразового кода
  • 00:25:59 - Спецификация OAuth 2
  • 00:28:04 - Доступ к разделам
  • 00:30:36 - Модерация клиентов
  • 00:35:33 - Refresh Token Grant
  • 00:36:25 - Дополнение PKCE

И в следующих эпизодах реализуем весь процесс аутентификации на бэкенде и фронтенде.

Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (20)
Максим

Спасибо))) Совсем скоро пойдёт домен! Ура)

Ответить
Руслан

Спасибо!

Ответить
Руслан

Дмитрий, извините за оффтоп, можете, пожалуйста, порекомендовать какой пакет лучше использовать для имплементации OAuth с PKCE в Laravel, для SPA с раздельным API приложением? Спасибо за Ваш труд!

Ответить
Дмитрий Елисеев

Для этого как раз есть Laravel Passport

Ответить
Руслан

Спасибо!

Ответить
Arunas

Спасибо. :)

Ответить
Максим

Дмитрий, подскажите, пожалуйста, почему в Value Object Id для генерации ID мы используем фабричный метод generate() вместо того, чтобы вынести это в сервис IdentifierGenerator и в нем метод `generate()’.

Тогда бы мы подключали этот генератор в командах и использовали бы примерно так:

$id = new Id($this->identifier->generate());

Это бы позволило вынести генерацию Id в отдельный сервис. Отдельно тестировать его и не давать VO больше отвественности, чем ему нужно...

Ответить
Дмитрий Елисеев

Да, можно при желании.

Имеет смысл переносить генерацию в репозиторий при использовании инкрементных идентификаторов, когда мы можем поместить туда доставание следующего номера из секвенции:

class UserRepository
{
    ...

    public function nextId(): Id
    {
        return new Id(
            (int)$this->connection->query('SELECT nextval(\'users_seq\')')->fetchColumn()
        );
    }
}

чтобы потом вызывать этот метод:

$id = $this->users->nextId();

Выносить в IdentifierGenerator имеет смысл если мы пишем юнит-тесты к хэндлерам команд и там хотим подменять этот генератор, чтобы он возвращал фиксированное значение.

Но как альтернативу этому можно генерировать значение прямо в контроллере и его передавать в команду в поле $id:

class Command {
    public string $id;
    public string $email;
    public string $password;
}

и потом брать его в хэндлере из команды:

$id = new Id($command->id);

Тогда $this->identifier->generate() или Uuid::uuid4()->toString() потребуется вызывать только в контроллере. Такой подход актуален если нам прямо из контроллера нужно будет сразу вернуть идентификатор. И в том случае, если команды у нас выполняются асинхронно.

Ответить
Максим

Да, я именно пришёл к тому, чтобы генерацию ID использовать даже не в командах, а контроллере. Иногда даже требуется генерировать ID на JS и передавать его в API. В любом случае спасибо за развёрнутый ответ)

А по поводу выноса в репозиторий - да. Видел у вас это. Однако, когда у нас есть отдельный сервис генерации ID мы можем реализацию метода generate() заменять как угодно. Если нужно - подключили в этот сервис репозиторий и запросили nextId(). Если не нужно сгенерируем Id Uuid::uuid4()->toString()

Ответить
Алескандр

С точки зрения безопасности иметь access token и refresh токен равносильно что иметь только один из них. Да, это удобно для перелогинивания пользователя через короткое время, чтобы обновить информацию в токене, например поменялась роль пользователя и когда истечет access токен вы используя refresh получите новый access токен и новые права доступа.

Но то что это делает процесс безопаснее - неправда, обычно рассматривают (как и в данном видео) ситуацию что хакер перехватил access token и у него только пару минут что-то там сломать и этого мало. На самом деле хакер перехватил и access и refresh токены, а далее используя refresh токен получает новый access токен раньше вас - в итоге у него полный доступ к вашему аккаунту, а вас разлогинело. И уже для решения таких атак нужно делать refresh token rotation и если дважды клиент обращается с одним и тем же refresh токеном то нужно инвалидировать все токены пользователя выпущенные после refresh токена

Ответить
alex

>> С точки зрения безопасности иметь access token и refresh токен равносильно что иметь только один из них.

Не всегда. По идее refresh токен нужно устанавливать в куки с флагом httpOnly, тогда js скрипт не сможет получить к нему доступ, а браузер сам сможет отправлять такой токен по маршруту который выдает новый access токен. При такой структуре получается что refresh token хранится в куках браузера, а не в приложении, поэтому он видится чуть более защищенным.

Ответить
Василий

Интересное решение, спасибо

Ответить
Tema

Где хранить access token в Local Storage, Session Storage, в локальной переменной внутри замыкания? Session Storage, внутри замыкания доступны только в одной вкладке. Остаётся Local Storage тк доступен между табами. Правильное ли это утверждение?

При попытке подменить данные в header-ре или payload-е, токен станет не валидным, поскольку сигнатура не будет соответствовать изначальным значениям. Так как секретный ключ для зашифровки лежит на сервере. Правильное ли это утверждение?

В access token не рекомендуется хранить какую либо sensitive data так как она не зашифрованная . Ок храним user_id, user_role в localStorage время жизни например 10 мин и отправляем в headere Authorization: Bearer token серверу

В refresh token храним в payloade user agent, fingerprint, ip, expiresIn в зашифрованном виде? Записываем в БД Записываем в Cookie (чревато атакой CSRF) решение иметь флаг Secure, httpOnly, Атрибут SameSite должен быть Strict + настроено Content-Security-Policy, X-Frame-Options, X-XSS-Protection, X-Content-Type-Options поможет ли Helmet? Нужен CSRF токен с учетом Fingerprint-a, maxAge куки ставим равную expiresIn, В path ставим корневой роут auth контроллера, время жизни например 30 дней

Зачем ставить куку с такими опциями /api/auth? чтобы запросы к публичной статике не содержали оверхед в header?

  • Если access token истёк?
  • Если refresh token истёк?
  • Если refresh token истёк а access token нет?
  • Если user agent другой
  • Если fingerprint другой
  • Если ip другой
  • Что если пользователь зашёл с двух устройств?
  • Реализовывать "Выйти на всех устройствах" в профиле юзера?
  • Что делать в бд с устаревшими токенами?
Ответить
Дмитрий Елисеев

Где хранить access token?

В случае только браузерного рендеринга хранить в Local Storage. Если же помимо браузера рендеринг производится и на JS-сервере по Server Side Rendering (SSR), то можно хранить в Cookies, чтобы токен был доступен и в браузере, и на сервере.

Ответить
Дмитрий Елисеев

При попытке подменить данные в header-ре или payload-е, токен станет невалидным?

Да. Токен станет невалидным, так как подпись уже не будет соответствовать данным.

Ответить
Дмитрий Елисеев

В refresh token храним в payloade user agent, fingerprint, ip, expiresIn в зашифрованном виде?

Хранить IP-адрес не очень удобно, так как со смартфона будет постоянно разлогинивать при каждом выходе из дома при переходе с WiFi на LTE и обратно.

Ответить
Дмитрий Елисеев

Если access token истёк?

То делаем запрос получения нового access token по refresh token.

Если refresh token истёк?

То очищаем его из своих Storage и при желании редиректим посетителя на страницу логина.

Если refresh token истёк а access token нет?

Такое маловероятно, так как refresh действует дольше access.

Если user agent другой. Если fingerprint другой. Если ip другой

То считаем такой токен невалидным.

Что если пользователь зашёл с двух устройств?

На каждом устройстве будут свои access и refresh токены.

Реализовывать "Выйти на всех устройствах" в профиле юзера?

Реализовали у себя в следующих эпизодах через очистку refresh-токенов по user_id.

Что делать в бд с устаревшими токенами?

Реализовали у себя очистку устаревших refresh-токенов и одноразовых кодов по Cron.

Ответить
Tema

Спасибо Дмитрий за столь исчерпывающие ответы!

Ответить
Денис

Дмитрий, а где можно скачать разные используемые вами материалы, например презентацию как в этом видео?

Ответить
Дмитрий Елисеев

А для чего она может понадобиться отдельно от видео?

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Google
GitHub
Yandex
MailRu