Аутентификация 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
Комментарии (22)
Максим (@myks92)

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

Ответить
Konstantin

Ну не так скоро конечно)

Ответить
Максим (@myks92)

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

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

Спасибо!

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

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

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

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

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

Спасибо!

Ответить
Arunas

Спасибо. :)

Ответить
Максим (@myks92)

Дмитрий, подскажите, пожалуйста, почему в 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() потребуется вызывать только в контроллере. Такой подход актуален если нам прямо из контроллера нужно будет сразу вернуть идентификатор. И в том случае, если команды у нас выполняются асинхронно.

Ответить
Максим (@myks92)

Да, я именно пришёл к тому, чтобы генерацию 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