Secure ID
Problem#
Любая система, которая имеет авторизацию на выполнение некоторых действий пользователем обязана в каждом методе выполнять не только логику, для которой этот метод описан, но и права доступа к данному объекту для данного пользователя. Это необходимо для того, что бы злоумышленники не могли отправить запрос к данным, которыми не владеет их учетная запись.
Таким образом, каждый запрос к системе обязан, перед исполнением, быть проверен на валидность (пройти авторизацию).
Зачастую, для выполнения этой задачи, системы выполняют некоторые запросы к подсистеме хранения данных, и по полученным
данным пытаются построить карту доступа или иным способом проверить то, что данный клиент имеет доступ к данной
сущности. Например, если пользователь пытается изменить элемент списка в туду -приложении, система должна будет
проверить, что данная учетная запись имеет доступ к данному туду-листу и к данной туду-строке. Конечно, в некоторых
случаях, автор приложения встроит проверку прав доступа прямо в запрос к конкретным данным (добавив в селектор что-то
типа where ... and (author_id=<CURRENT_USER_ID>')
). Это просто пример, в котором проверку доступа к объекту
(при его чтении или изменении) выполняет по-сути СУБД.
Эти операции стоят времени и других ресурсов на их исполнение, а так же в каком-то смысле дублируют логику системы. Дубль происходит между запросом на чтение данных и запросом на их изменение.
В первом запросе (read query) система выбирает требуемые данные из СУБД с какими-то фильтрами типа “объект опубликован/доступен/etc”. И в качестве ответа отдает список данных объектов. Эти фильтры (для выборки данных) включают себя проверку прав доступа пользователя к этим объектам.
Во втором запросе (mutate query) система получает от клиента идентификатор сущности, которую тот собирается изменить. Для того, что бы система убедилась в том, что данный пользователь может выполнять данное действие с этой сущностью - система выполнит ряд запросов типа тех, что перечислены далее (не обязательно по одному запросу на проверку - главное, что системе нужно выполнить эти проверки так или иначе):
- получить объект по id (он вообще существует в таблице?)
- проверить что он опубликован/доступен/etc (его вообще кто-то мог увидеть до текущего mutate запроса?)
- получить связанный объект по id (например, что бы подтянуть доп данные для принятия решения)
- проверить, что родительский объект привязан к аккаунту текущего пользователя (текущий пользователь имеет права на mutate?)
По факту система выполняет обратную проверку данных, которые она получила в запросе на чтение данных. Повторюсь - система выполнила ряд фильтраций при исполнении запроса на чтение, и ровно те же запросы (в плане объема и сути получаемой информации) ей придется исполнить вновь, что бы проверить доступы на исполнение запроса на изменение. Чуть выше я сравнил эту проблему с дублированием логики, но возможно это даже хуже. Это переписывание логики проверки доступа к данному действию над объектом -звучит неприятно и опасно. И неэффективно.
Solution#
Для того, что бы описать решение, нужно понять особенность привычных нам систем и понять, почему же системы строятся именно так.
Системы вынуждены заниматься такими операциями для того, что бы проверить, что запрос легитимен и его не подделал злоумышленник (например, вызвал метод удаления для объекта, который ему не принадлежит).
Если упростить данную тему и создать некоторое утверждение - то получится, что система не доверяет входящим данным. И это правильно. Однако, если посмотреть дальше - окажется, что данные, которым не доверяет система - это именно те данные, которые она же сама и отдала клиенту. Неловкая ситуация.
Но что, если система научится передавать данные от себя к самой же себе через недоверенного клиента? Причем передавать эти данные так, что будет доверять им после получения их обратно.
Secure ID#
Для того, что бы система начала доверять данным “из интернета” - она должна научиться передавать данные сама себе через недоверенные источники. Способов передачи данных таким образом довольно много. В примере рассматривается именно JWT, в надежде, что с ним знакомо большинство. Естественно, заменив его на что-то, что может подписывать данные или шифровать данные - концепт никак не изменится. Отсюда же следует и то, что не стоит на данном этапе бояться размера получаемых данных. Сейчас нам больше всего важен - комфорт, наглядность и бекграунд работы с JWT.
Представим, что все идентификаторы, которые система передает на клиент - строковые. Клиент получает некоторую строку и
для выполнения действия с данным объектом - отправляет ее обратно. (прим. получила твит и его идентификатор; для
изменения/удаления нужно отправить обратно идентификатор и название метода - delete
).
Система может положить в эту строку все, что угодно. Представим, что она отправляет (и принимает) jwt строку вместо id, в которой есть этот идентификатор и доп информация. Конечно же, JWT подписан и только у системы есть ключ. Для наглядности - вот минимальный пример пейлоада:
{
"id": "tweet-42"
}
Пока в Secure ID находится только идентификатор сущности - он почти ничем не отличается от варианта без JWT. Для того, что бы изменения вступили в силу необходимо добавить дополнительные поля.
Добавив в Secure ID идентификатор текущего пользователя (который запросил эти данные), мы сможем проверять этот идентификатор на факт того, что конкретный аутентифицированый пользователь получил этот идентификатор в одном из предыдущих запросов и только он может что-то делать с этим id. Да это дублирование данных из токена пользователя, однако важнее - мы получили гарантию того, что этот твит был отдан именно этому пользователю по именно его запросу.
{
"id": "tweet-42",
"viewer_id": "user-1"
}
Помимо этой информации, конечно же можно добавлять все, что необходимо (в разумных пределах в зависимости от особенностей и ограничений приложения). Так же не обязательно использовать подобный подход для генерации идентификаторов (это иногда может создать серьезные сложности). Можно использовать этот подход, что бы генерировать некие action-токены, которые будут передаваться клиентом для совершения соотв действия с объектом.
Если все идентификаторы будут конструироваться подобным образом, то системе больше не придется тратить время (и создавать потенциальные уязвимости) на проверку авторизации данного клиента к данному объекту. Как минимум - она может быть уверена в том, что данный пользователь однозначно смог “увидеть” данный твит ранее. Система должна только лишь проверить подпись и проверить совпадение идентификатора пользователя и идентификатора текущего (запрашивающего действие с этими данными) пользователя из токена.
Отлично, теперь каждый пользователь, который смог увидеть данные - может что-то с ними сделать. Мы не выполняем никаких запросов с целью перепроверить доступы данного пользователя к данному объекту. Profit.
NOTE: в этой схеме есть ряд ограничений, применимых к конкретным кейсам в конкретных системах, например полученный идентификатор гарантирует, что пользователь имел доступ к объекту, однако к моменту вызова mutate запроса доступ мог уже пропасть. Есть и неудобства, которые так же обходятся (но они все же есть) - например в фиче “поделиться твитом” клиентскому приложению нужно будет обработать кейс, что из персонального идентификатора твите (Secure ID) нужно сделать просто id типа
tweet-42
. В некоторых случаях будет необходимо добавлять доп информацию в payload (например версию объекта), илиallowed_actions: ['delete','update','block']
.
TODO: описать более подробно, с примерами псевдокода или кода.