О чем эта статья: статья посвящена схемам регистрации, аутентификации, авторизации пользователей в веб приложениях, смене паролей, отзыве токенов, и т. п. Мы также рассмотрим, как организовать хранение данных, обсудим, что такое JWT, коротко поговорим о «сервисе для отправки email»
Почему я так назвал статью: в любом приложении существует ряд процедур напрямую не связанных с его задачей. Это методы, относящиеся к выдаче прав доступа и отзыву этих доступов. На данный момент не существует единого термина, охватывающего их все. В этой статье, я постарался перечислить эти процедуры, и описать их в виде графических схем. Насколько мне это удалось — увидим.
Для кого эта статья: для тех, кто начинает осваивать методы аутентификации и авторизации в веб приложениях и хочет понять, как все устроено. Я намеренно избегал использования комплексных библиотек и фреймворков, чтобы на пальцах разобраться, какие методы нужны в Auth сервисе и как их использовать.
Статья будет состоять из 2 частей. В первой части мы обсудим теорию, нужную для понимания приведенных методов, а во второй будут перечислены основные эндпоинты Auth сервиса и даны схемы с комментариями. Если вы уверены в своих силах, и не хотите читать нудную теорию, можно сразу перейти к разделу эндпоинты, если нет — продолжим.
Термины
Идентификация — процесс получения идентификатора пользователя: логин/e-mail/id
Аутентификация — подтверждение личности пользователя подтверждение личности пользователя (с помощью пароля, отпечатка пальца, и т.п.)
Авторизация — предоставление прав пользователю, выдача токена
Валидация — процесс проверки «куска» информации на соответствие требованиям программы, или просто на совпадение с копией, хранимой в базе данных.
Auth — общий термин для обозначения сервисов / модулей / эндпоинтов, отвечающих за идентификацию / аутентификацию / авторизацию в приложении. Рускоязычного аналога нет.
JWT (Json Web Token) — ключ аутентификации пользователя
Code — код подтверждения. Высылается на почту пользователя для подтверждения регистрации или сброса пароля.
Credentials — учетные данные пользователя: логин, пароль, google id, и т.п.
Эндпоинт — url адрес конкретного метода API или Auth сервиса
Метод — в рамках данной статьи = эндпоинт
Методы API — методы, реализующие задачи приложения (то, для чего мы его написали)
Методы Auth сервиса — методы, напрямую не относящиеся к задачам приложения (в рамках этой статьи противопоставляются «методам API»)
БД — база данных
Email-client — почтовый сервис, например: gmail, yandex mail, и т. п.
TTL (Time to live) — время жизни записи в БД (например: Redis)
Redis — сервер баз данных типа ключ-значение. Очень быстрый, идеально подходит для хранения кеша и токенов (документация тут)
Клиент — уровень представления данных (см клиент-серверная архитектура). Имеет графический интерфейс для взаимодействия с пользователем. пример: веб-сайт в интернете.
Сервер — уровень получения и обработки данных (см клиент-серверная архитектура). Не имеет графического интерфейса, общается со многими киентами через API.
Auth service
Как было сказано выше, Auth сервис включает в себя процедуры, связанные с выдачей прав доступа пользователям, и отзывом этих доступов. Перечислим эти процедуры:
регистрация пользователя с логином и паролем (схема)
отправка кода подтверждения на e-mail пользователя (схема)
подтверждение регистрации с помощью кода, полученного на email (схема)
регистрация пользователя через посредника: гугл, фейсбук, и т.п. (схема)
аутентификация и авторизация пользователя с логином и паролем (схема)
аутентификация и авторизация пользователя через посредника (схема)
выход из аккаунта, отзыв токенов (схема)
сброс пароля (пользователь еще не авторизован) (схема)
обновление пароля (пользователь авторизован) (схема)
выдача токенов после успешной авторизации (схема)
обновление истекших токенов (схема)
поддержание блэк-листа токенов (схема)
контроль версий учетных данных (схема)
Разумеется, это список можно расширить. Сюда можно было бы добавить еще:
определение ролей пользователей
авторизацию по номеру телефона
двухфакторную аутентификацию
удаление учетной записи
и т.п.
К сожалению, формат статьи не позволяет описать все за один раз, поэтому сегодня мы ограничимся первыми 13 пунктами.
Структура данных
При проектировании приложения важно внимательно продумать хранение данных. Личные данные пользователя (никнейм, аватар, и проч), и учетные данные (логин и пароль) не должны пересекаться в одной таблице. Почему это важно:
разделение ответственнности: только методы Auth сервиса должны иметь доступ к учетным данным пользователя, остальным сервисам эти данные не нужны. Меньше точек доступа => выше защищенность приложения.
расширяемость: это позволяет использовать несколько типов авторизации в одном приложении и добавлять новые методы авторизации по мере необходимости
масштабируемость: в крупных системах может быть несколько приложений с авторизацией через один общий Auth сервис. В таком случае, учетные данные вообще хранятся в отдельной БД принадлежащей только этому сервису, другие сервисы не имеют туда доступа.
Вопрос: а что делать с email пользователя, который часто используется в качестве логина, но так же нужен нам для отправки писем с подтверждением регистрации и кодов для сброса пароля?
Ответ: email используется, как логин только по одной причине — уникальность. На мой взгляд, правильным будет его продублировать в таблицах «users» и «credentials», поскольку здесь он выполняет 2 не связанные между собой функции: логина и email.
Для примера, пусть в нашем приложении будет:
авторизация по паролю
авторизация через гугл
авторизация через facebook
![структура данных Auth сервиса структура данных Auth сервиса](https://habrastorage.org/getpro/habr/upload_files/ad1/a52/b7f/ad1a52b7f458809df65d3aab5580444c.png)
JWT
Json Web Token — ключ аутентификации пользователя. Используется для запросов к защищенным методам API.
Я уже описывал нюансы работы с Jwt в статье «Подробно про JWT», поэтому не будем останавливаться на этом. Если приведенные ниже схемы вызовут у вас вопросы, ознакомьтесь с разделами стати про JWT:
Валидация токенов
Black-list токенов
Контроль версий
Коротко про почтовый сервис
Для того, чтобы реализовать процедуры регистрации / авторизации силами приложения вам понадобится собственный почтовый сервис. Он необходим для подтверждения регистрации учетной записи, и для отправки ссылок на сброс пароля на почту пользователя.
Реализовать такой сервис не сложно, большинство сред разработки имеют готовые библиотеки для этого, но есть нюанс: в современном мире уже нельзя просто отправить письмо со случайного IP из кода приложения. Такие письма будут развернуты email-клиентом (gmail, yandex mail, и т.п.), как подозрительные. Они не просто упаут в спам, они вернутся с ошибкой доступа (!)
Чтобы письма доходили до адресата нужно подключать сертифицированный сервис-посредник, удостоверяющий подлинность домена и самого письма, например: aws.amazon.com. Процедура не сложная, но муторная, а если вы планируете большой объем рассылки (>200 писем в день), то еще и платная.
Вывод: если вы хотите быстро выложить свое приложение на продакшн и нет времени возиться, рекомендую подключать авторизацию через Google или Facebook, на первых порах это проще.
Эндпоинты
Итак, для реализации Auth сервиса нам понадобятся следующие эндпоинты:
POST ~/auth/sign-up — зарегистрировать пользователя с логином и паролем (схема)
POST ~/auth/send-code — отправить email с кодом повторно (схема)
POST ~/auth/confirm-email — подтверждение регистрации пользователя (схема)
POST ~/auth/sign-in — авторизоваться в системе (схема)
POST ~/auth/logout — выход из аккаунта (схема)
POST ~/auth/reset-password — сброс пароля (схема)
POST ~/auth/confirm-reset — подтверждение сброса пароля (схема)
PATCH ~/auth/update-password — обновить пароль (схема)
POST ~/auth/refresh-tokens — получить свежую пару токенов (схема)
Регистрация пользователя
![регистрация пользователя. схема регистрация пользователя. схема](https://habrastorage.org/getpro/habr/upload_files/cf9/ee6/d0c/cf9ee6d0c5339c507460d059b8847bf3.png)
Что тут происходит:
ввод данных пользователя, проверка уникальности email
шифрование пароля, сохранение данных пользователя в БД
генерация кода подтверждения, сохранение кода в Redis БД
получение кода подтверждения на почту пользователя
запрос на подтверждение регистрации пользователя, сохранение пометки в БД
генерация JWT токенов, передача на клиент
Вопрос: для чего нужно «подтверждение регистрации пользователя»?
Ответ: по сути, это проверка валидности email, который понадобится для сброса пароля.
Отправка кода подтверждения повторно
![отправка кода подтверждения. схема отправка кода подтверждения. схема](https://habrastorage.org/getpro/habr/upload_files/5bc/e1a/9a7/5bce1a9a71502d4b5cd8ad47cef243c1.png)
регистрация пользователя через google Что тут происходит:
клик по кнопке «выслать код повторно», проверка email, подтверждение email
генерация кода подтверждения, сохранение кода в Redis БД
получение кода подтверждения на почту пользователя
Вопрос: для чего нужно повторно отправлять код?
Ответ: чтобы коды не накапливались в кеше, любой код подтверждения должен иметь короткое время жизни (в среднем - 15 мин). Если за это время пользователь не успел завершить регистрацию, ему потребуется повторно запросить код.
Регистрация пользователя через google
![регистрация пользователя через google. схема регистрация пользователя через google. схема](https://habrastorage.org/getpro/habr/upload_files/271/d97/76c/271d9776c6f0b55b0a4e837437330307.png)
Что тут происходит:
запрос авторизации через google, перенаправление на страницу авторизации, ввод credentials
сохранение данных пользователя в БД.
генерация JWT токенов, передача на клиент
Вопрос: как сервер отличит запрос на авторизацию c логином и паролем от регистрации через google аккаунт, если url один и тот же?
Ответ: я передаю тип регистрации в теле запроса. Параметр «providerType», может принимать значения: local, google, facebook, и т.п.
Авторизация пользователя
![авторизация пользователя. схема авторизация пользователя. схема](https://habrastorage.org/getpro/habr/upload_files/d0f/e60/c44/d0fe60c44201d60c363d9e3dc144702d.png)
Что тут происходит:
ввод пользователем логина и пароля, проверка логина и пароля сервером
генерация JWT токенов, передача на клиент
Авторизация пользователя через google
![авторизация через google авторизация через google](https://habrastorage.org/getpro/habr/upload_files/f24/a1c/e19/f24a1ce1974c154f561bfda4e6cd98a5.png)
Что тут происходит:
запрос авторизации через google, перенаправление на страницу авторизации, ввод credentials
получение данных пользователя из БД.
генерация JWT токенов, передача на клиент
Вопрос: как сервер отличит запрос на авторизацию c логином и паролем от регистрации через google аккаунт, если url один и тот же?
Ответ: я передю тип авторизации в теле запроса. Параметр «providerType», может принимать значения: local, google, facebook, и т.п.
Выход из аккаунта
![выход из аккаунта. схема выход из аккаунта. схема](https://habrastorage.org/getpro/habr/upload_files/5f0/3eb/69a/5f03eb69a5ddbd89b8415cecb9fe6174.png)
Что тут происходит:
Пользователь нажимает «Выход», приложение передает в Auth сервис токены
Сервис добавляет токены в black-list, чтобы по ним нельзя было войти в приложение
Пользователь перенаправляется на страницу авторизации
Сброс пароля
![сброс пароля. схема сброс пароля. схема](https://habrastorage.org/getpro/habr/upload_files/686/ebe/744/686ebe744ec97944f985ee109f38ca4f.png)
Что тут происходит:
Пользователь нажимает «Сбросить пароль», переадрес на страницу ввода email
Пользователь вводит email для получения ссылки, серевер проверяет валидность email
Auth серевер создает код для сброса пароля, отправляет на почту пользователя
Пользователь получает код
Пользователь вводит новый пароль и код подтверждения, Auth серевер валидирует код, хеширует пароль, сохраняет хеш пароля в БД
Auth серевер генерирует новую пару токенов, отдает на клиент.
Обновление пароля
![обновление пароля. схема обновление пароля. схема](https://habrastorage.org/getpro/habr/upload_files/52a/01e/514/52a01e51422825748c08357f5ae12429.png)
Что тут происходит:
Пользователь переходит в раздел "Профиль пользователя" в приложении, вводит старый пароль, новый пароль.
Извлекаем uuid пользователя из токена, получаем хеш текущего пароля из БД, валидируем пароль
Шифруем новый пароль, сохраняем в БД хеш нового пароля, хеш старого пароля, увеличиваем версию учетных данных на +1 (см статью «Подробно про JWT»)
Заносим токены в black-list, чтобы по ним больше нельзя было войти в приложение
Генерируем новую пару токенов, отправляем на клиент
Обновление JWT
![обновление JWT. схема обновление JWT. схема](https://habrastorage.org/getpro/habr/upload_files/c5f/967/eb8/c5f967eb85891a47341cbaf045511e6d.png)
Что тут происходит:
запрос данных с access токеном
валидация access токена не прошла, возврат ошибки #401
запрос обновления токенов с refresh токеном,
проверка, не внесен ли токен в black-list
получение версии учетных данных из БД
валидация токена, проверка версии токена
генерация новой пары токенов, отправка на клиент
В заключение
Приведенные практики не являются истиной в последней инстанции. Существует ряд подходов к реализации Auth сервисов, и дополнительные хитрости для увеличения защищенности приложения. Я написал этот текст, чтобы самому разобраться что к чему. Если вы нашли ошибку в моих рассуждениях, буду признателен за любые комментарии.
Для тех, кто пишет на TypeScript + Nest.js могу написать статью с примером реализации. Если вам это будет интересно, пишите.
Еще по теме
Role-Based Access Control (RBAC) — роли пользователей
OAuth 2.0 — протокол, для организации Auth сервиса в виде отдельного сервера. Берет на себя выдачу JWT и упрощает определение прав доступа пользователям.
OpenID — механизм аутентификации на множестве не связанных друг с другом ресурсов основанный на OAuth 2.0
PXCE — дополнительный уровень защиты для мобильных приложений
Elanlum
Отличная статья получилась. От себя отмечу пару моментов:
- В статье явно говорится о хэшировании паролей, но в тексте прямо написано "шифрование". Уверен, автор понимает, в чем разница и что именно хэширование применяется для паролей по причине невозможности восстановления исходной строки, это безопаснее.
- не лишним было бы вкратце упомянуть, для чего нужны токены, которые Auth сервис как провайдер авторизации возвращает. Хотя бы то, что этими токенами потом сопровождается каждый запрос в авторизованной зоне приложения, в свою очередь по содержимому токена приложение определяет, что токен не истек и выдан тому же клиенту, который вызывает эндпоинт в авторизованной зоне