Аутентификация
LINEМы каждый день сталкиваемся с аутентификацией в интернете. Эта статья о том, что это такое, какие факторы аутентификации есть и как подтвердить что вы - это вы.
Терминология
Давайте для начала разберемся со всеми терминами, которые могут встретиться, а именно
- идентификация
- аутентификация
- авторизация
- регистрация
- верификация
С идентификацией мы знакомы ближе всех. Это просто способ указать на конкретный объект. Учитель, который называет в классе ученика по имени (при условии, что ученик с таким именем один) однозначно идентифицирует ученика. Мы каждый день идентифицируем друзей и близких по внешности. Государство выдает различные документы с уникальными идентификаторами - серия номер паспорта, номер счета и т.д.
Для идентификации чего-либо в программе используется в основном или просто число, или строка из букв, чисел и специальных символов. Для каждой записи в таблице базы данных должен существовать уникальный id. Числовые идентификаторы реализовать проще, т.к. нужно просто выдавать следующее натуральное число. Идентификатор не должен повторяться.
С идентификаторами в виде строки мы также сталкиваемся часто в виде username. Кроме того, существует такое понятие как slug - это замена числового идентификатора (статьи, заметки, категории), в основном используется в url адресе. Просто url /Autentifikaciya-03-16 (такой url имеет эта статья на telegra.ph) понятнее чем, например, post/123456 (такой url на хабре).
Аутентификация - проверка подлинности. Из этой же категории слова аутентичный (authentic - подлинный, достоверный), автор (author). В жизни мы также сталкиваемся с проблемами аутентификации - когда нужно убедиться что эти кроссовки оригинальные, письмо пришло действительно от нужного человека. Достигается она простым способом - вы должны сказать информацию, которую должны знать только вы.
Авторизация - проверка прав доступа. От англ. authorize - разрешать, авторизовывать. В большинстве систем, в большинстве компаний каждый человек или подсистема имеет свою роль. Возьмем простой пример блога - у блога есть читатели, которые просто читают статьи, писатели - те, кто может создавать новые статьи и администраторы - те, кто обладают большими возможностями. Как только вы подтвердили что вы - это вы (например ввели логин и пароль), система выдает вам определенную роль, которая определяет на что у вас есть права.
Регистрация - добавление нового пользователя (или другой сущности). Для того чтобы идентифицировать личность, нам нужны начальные знания о человеке. Представим что к вам на улице подойдет незнакомец. Как вы можете доказать что он, это он? Регистрацию в таком примере можно сравнить со знакомством.
Верификация - проверка данных по определенным критериям, соответствие формату. Если вы дадите мне номер телефона - он может быть неверным. Но если вы дадите номер в котором будет буква - он точно будет неверным.
Все эти термины часто путают и говорят об авторизации как об идентификации, аутентификации и авторизации вместе. Даже переводчики. Например, на очень полезном сайте wooordHunt перевод authentication обозначен как идентификация.
Факторы аутентификации
В целом весь процесс аутентификации построен на одном свойстве - у вас должно быть что-то, чего не должно быть ни у кого больше. В основном выделяют следующие фактора аутентификации, т.е. тип этого "что-то":
- На основе знания чего-либо (Authentication by Knowledge) - примеры это пароль, пин-код, секретный ключ
- На основе обладания чем либо (Authentication by Ownership) - например, физический ключ от двери, физическое устройство для пропуска
- На основе биометрических характеристик (Authentication by Characteristic) - сетчатка глаза, отпечаток пальца, структура тканей, голос, почерк и т.д. Понятно что этот тип относится к человеку. Их также можно разделить на статические и динамические. Сетчатка глаза не поменяется со временем (ну или почти), а вот частота, высота и другие особенности голоса могут измениться как со временем, так и, например, во время стрессовых ситуаций.
Можно также отнести
- На основе "контекста". Например, по определенным ip или mac адресам, географическому местоположению, времени и т.д.
- На основе удостоверения третьей стороны, которой все доверяют.
Многофакторная аутентификация
Здесь все понятно - это аутентификация на основе нескольких факторов. Конечно нужно чтобы эти факторы были разных типов и на основе значения одного типа нельзя было получить значение другого типа.
Частным случаем является двухфакторная аутентификация, при которой вам нужно указать 2 фактора. В основном это какая-то информация (пароль, код, данные карты) и еще одна информация. Это может быть код на почту, смс, назвать номер который вам сейчас позвонит и т.д.
Защита информации должна соответствовать важности информации. Возможно для сайта на котором хранятся тексты песен вам достаточно одного пароля, но для приложений, связанных с финансами, секретной информацией факторов должно быть минимум 2.
Парольная аутентификация
С этим видом аутентификации мы встречаемся чаще всего, и его мы рассмотрим подробнее. Он представляет собой сравнение пароля, введенного пользователем с паролем, хранящемся в базе данных. Этот тип довольно простой и главное привычный.
Разделяют аутентификация по одноразовым и многоразовым паролям. Одноразовый пароль говорит сам за себя - с его помощью можно пройти аутентификация только один раз (вы пользовались чем-то подобным когда забывали свой многоразовый пароль и с помощью почты или номера телефона восстанавливали возможность зайти в аккаунт).
Рассмотрим сценарий входа по многоразовому паролю в некоторый сервис. Неважно что это за сервис и какие услуги он предоставляет, назовем его А.
У нас есть отдельный сервер, где работает приложение и один клиент.
Первым делом на сервере появится база данных и таблица следующего вида. В ней будет 4 поля: id, имя пользователя, пароль и роль.

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

Вы вводите имя, пароль и подтверждение пароля. Все эти данные передаются на сервер, где сначала проверяется не существует ли уже данный username (он должен быть уникальным - это идентификатор пользователя), и если все в порядке то пароль имя и пароль заносятся в базу данных. Поле id заполняется автоматически СУБД, а роль позволяет описать разрешенные пользователю действия. Таким образом пройдя аутентификация мы пройдем и авторизацию, т.е. получим возможность взаимодействовать с сервисом.
Пусть некий пользователь Mark с паролем qwerty зарегистрировался. Значит на сервере есть таблица users со следующими значениями. В чем проблема такого подхода - в том что если кто-то сможет посмотреть всю таблицу он увидит и username, и пароль. Украсть из такой таблицы пароль очень легко (надо признать что время от времени по тем или иным причинам у различных сервисов "утекают" таблицы с пользователями)

На самом деле нам не нужно хранить пароль, нам нужно знать что пользователь ввел верный пароль. Здесь на помощь приходят алгоритмы хеширования.
В самой таблице будет храниться на сам пароль, а его хеш. Хеш функция (hash function) - то такая функция, которая на вход принимает произвольную последовательность байт, а на выходе выдает последовательность байт определенного формата и размера.
Возьмем модуль hashlib из python и посмотрим на алгоритмы, которые уже в нем реализованы.

Выберем sha256. Функция hashlib.sha256 возвращает хеш от переданного значения. На вход она получает поток байт, а не строку, поэтому сначала переведем строку в поток байт с помощью encode(). Мы получим объект hashed_password, у которого есть функция hexdigest, которая выводит хеш в шестнадцатеричном формате.

Теперь таблица users выглядит следующим образом (хеш сокращен)

Да, мы все еще можем потерять таблицу пользователей. Но особенность hash функций - их однонаправленность, т.е. из пароля получить хеш легко, а из хеша получить пароль - (почти) невозможно.
В какой-то момент создатели сервиса А решили создать новые сервис, который они назвали В. Проделав все те же операции, они заметили что люди приходят с сервиса А на В, регистрируются и указывают те же имена и пароли. Давайте сравним две таблицы двух разных баз данных в А и В.

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

Поэтому было принято решение добавить еще некоторою информацию к каждому паролю в каждой таблицу. Для таблицы users в A выберем букву "А", а для B - "B" (конечно лучше сделать их подлиннее). Теперь мы будем хранить не хеш пароля, а хеши строк вида "А<password>" и "B<password>". Такие добавочные фразы называются солью (salt) и можно встретить фразу "посолить пароль".
Попробуем найти хеш теперь

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

Все хорошо, но в какой-то момент на сервисе A зарегистрировался другой пользователь. Так получилось, что его пароль также "qwerty", и такая же соль, а значит опять мы получим одинаковый хеш. Чтобы исправить эту ситуацию, мы будем добавлять 2 вида соли - статическую и динамическую. Статическая соль - одинакова для всех, а динамическая - у каждого своя. Пусть это будет просто id таблицы users (ведь мы знаем username и можем достать id). Тогда итоговое значение для хранения строка вида salt + id + password

Теперь, даже если пароли совпадают, понять это по хешу нельзя.
Практика
Я постараюсь сильно упростить и показать только самую суть данного процессов идентификации, аутентификации и авторизации в веб приложениях. Для этого используем фреймворк Flask, иногда Chrome и иногда Postman.
Начнем с приложения Flask. Установим его, создадим файл app.py и добавим туда следующий код. В первой строке импортируем Flask, создаем объект типа Flask, добавляем один контроллер на путь "/", который возвращает ОК.

Переходим в браузер по адресу http://127.0.0.1:5000 (127.0.0.1 - адрес локального хоста, 5000 - порт по умолчанию) и видим там строку ОК.
Начнем с регистрации. Добавим новый путь /register, по которому будем регистрировать новых пользовалетей. Напишем новую функцию контроллер register (пропустим шаблоны для краткости и вернем html напрямую). Также добавим словарь users (userts = {}), который будет заменять нам базу данных. Ключем будет username, а значением - password.

Эта функция будет обрабатывать сразу 2 метода - и GET и POST. Перейдем по адресу http://127.0.0.1:5000/register и увидим следующую форму.

Это нам пришел ответ от запроса GET. Как только мы заполним поля username и password, браузер сделает запрос на сервер по адресу, указанному в action методом пост и введенные данные отправит в теле запроса (не забываем проверять поля на пустые строки, добавить поле подтверждение пароля и т.д.). Заполним username как Mark и password как qwerty и нажмем на кнопку register.
Мы можем посмотреть на отправленные данные (они также должны отобразиться в консоли, в методе register есть print(users)). Для этого откроем консоль разработчика, перейдем на вкладку network, выберем вкладку headers и внизу будет form data. Все верно, то что мы отправляли. Flask позволяет получить эти данные через словарь form, который есть у объекта request. Flask заботливо получил весь запрос в виде набора байт, сам его распарсил и положил нужные значения по нужным переменным. Обращение request.form['username'] - это обращение к запросу (request), у которого есть форма (from) у которой есть значения input с name=username и name=password. Теперь мы можем сохранить username нового пользователя и хеш пароля в базу данных (мы положили их в словарь users, и пароли оставили в открытом виде для простоты. Конечно нужно положить их хеш). Мы зарегистрировали нового пользователя.

Чтобы перейти дальше, стоит сказать о таких понятиях как stateless и statefull архитектура. Дело в том, что изначально протокол http не хранит состояния о запросе сам по себе (stateless - без состояния, но мы сами можем добавить поведение с состоянием). Т.е. вы делаете запрос на сервер, он отрабатывает и возвращает ответ. Но если вы сделаете запрос снова, то как сервер сможет понять что это снова вы?
Немного подумав, мы придем к простому, но очень некрасивому решению - посылать username и password с каждым запросом. Такой вид аутентификации называется basic.
Давайте добавим новый путь вида /resourse. Пусть по этому пути лежит какой то ресурс, но посмотреть его можно только в том случае, если вы прошли аутентификацию.

Вот что здесь говорится. Мы создаем ответ с кодом 401 и текстом Unauthorized, добавляем заголовок WWW-Authenticete: Basic. Этот заголовок говорит о том, как пройти аутентификацию (basic), realm - это параметр, который отвечает на какой части сервера нужна данная аутентификация, сейчас он не важен.
Как только ответ вернется браузеру он сам отрисует следующее окно.

Вы вводите имя и пароль, после чего нажимаете sign in. Если посмотреть что на то, что браузер отправляет серверу, то мы увидим следующий заголовок (Authorization: Basic и что-то на эльфийском).

Берем эту строку (TWFyazpxd2VydHk=), декодируем с помощью base64 (base64 - способ представления любого файла в виде строки. Работает он так - берем 3 байта, т.е. 3 * 8 = 24 бита, делим на 4 фрагмента по 24 / 4 = 6 бит и каждый такой фрагмент заменяем на символ в таблице кодировки base64)и получаем Mark:qwerty. Да, эта строка закодированная строка вида <username>:<password>. Теперь мы можем при получении раскодировать, разделить по символу ":" и получить данные. Важно, что и username и password передаются при каждом запросе и проверять их нужно также при каждом запросе.
Добавим проверку на наличие заголовка Authorization, а также добавим авторизацию (но уберем пока проверку пароля, для краткости. Она должна быть). У нас есть имя пользователя, получим из таблицы значение role для имени и если role равна admin (для примера), то разрешаем доступ. Если нет - запрещаем. Мы не должны запрещать доступ, так мы просто можем не дать обычному пользователю выполнить опасную операцию по-определенному url. Так мы проводим авторизацию.

Парольные политики
Рассмотрим рекомендации к паролям со стороны сервера и клиента
Со стороны сервера
- использовать статическую и динамическую соль
- пароль при регистрации аккаунта должен иметь не менее 8 символов (число 8 ничем не обосновано), хотя бы одну заглавную букву, цифру и спец символ
- не распространять информацию об алгоритме хеширования
- сократить число людей имеющих доступ к таблицам связанными с паролями
- ограничивать число попыток и частоту попыток, например не больше 5 попыток входа в минуту
- логировать все неудачные попытки входа
Со стороны клиента
- создавать сложные пароли
- не использовать одинаковые пароли на разных сервисах
- не проходить авторизацию в общедоступных сетях, которым вы не доверяете
- держать пароль в секрете (а не в файле пароли на рабочем месте)
Токены (tokens)
Посмотрим на недостатки basic аутентификации. Мы не можем кастомизировать страницу входа, логин и пароль передаются в открытом виде, у нас не так много возможностей для того, чтобы выйти из учетной записи (т.е. есть возможности сбросить заголовка Authorization, но в основном его контролирует браузер).
Всего для протокола http у нас есть несколько вариантов, как мы можем отправить данные. Это
- url
- body - тело запроса
- header - заголовки запроса
Но ведь заголовок - это просто строка. Почему мы не можем сами добавить новый заголовок? Можем, давайте так и сделаем. Добавим новый путь resourse2 и новый обработчик, который просто выведет успех или неудача в зависимости от наличия заголовка my_auth (но конечно мы сможем получить и значение этого заголовка). Конечно пока такого заголовка у нас нет.

Теперь возьмем Postman и вручную добавим заголовок my_auth

Ответ Success. Вот что мы сделаем теперь. Мы добавим новый путь /login, в котором отрисуем форму для входа по запросу GET. При введении данных в форму и отправке по POST методу мы сгенерируем токен, т.е. просто строку. Для этого мы возьмем время, добавим имя пользователя и получим md5 хеш этой строки. В целом неважно как будет создан хеш (но важно, чтобы он был уникальным). После этого сохраняем этот хеш в словарь tokens(в базу данных), где хеш будет ключем, а значение - имя пользователя. После отправляем его обратно клиенту прямо в теле запроса (или в заголовке, или в формате json).

Добавим еще один путь /resourse3. И теперь мы предполагаем, что с каждым запросом пользователь будет отсылать нам токен в заголовке "my_auth", по которому мы его распознаем.

Делаем запрос с помощью Postman, не забываем добавить в заголовок токен который нам пришел от /login

И вот сервер действительно нас узнает. Так мы научили узнавать нас сервис с помощью токена.
Куки (Cookies)
Согласитесь, каждый раз вручную добавлять заголовок не то, чего мы хотим. Поэтому есть специальный механизм, называемым cookies - в целом он не отличается от заголовков, просто это отдельный заголовок для пар ключ-значение. Его особенность в том, что сервер может установить cookie и браузер сам будет добавлять этот cookie при каждом запросе. Изменим немного страницу login, заменив возвращение токена на сохранение токена в cookie (в Flask это делается с помощью set_cookie)

Теперь по запросу пути /resourse3 мы проверяем, есть ли такая cookie

Смотрим заголовки и видим там cookie, которые браузер отправил автоматически.

Cookie предоставляют больше возможностей, чем описано здесь. Их можно установить на определенное время (expires), области видимости т.е. на какие url они будут отправляться (domain, path), к ним можно обратиться из javascript (отличие составляет параметр httponly), их можно настроить на отправку только по https (secure), в любой момент сервер может их удалить. В cookies можно хранить произвольные данные вида ключ-значение, но не забывайте что они будут отправлены на сервер каждый раз, т.е. это не база данных - хранить какой-нибудь id - хорошо, хранить какой-нибудь список - плохо.
Сессии (sessions)
На базе cookie можно реализовать механизм сессий, т.е. каждый раз когда вы будете отправлять серверу запрос с определенным cookie, сервер поймет что это вы. Но вам не обязательно быть даже зарегистрированным пользователем, сессия может быть анонимной. Просто так сервер может снова вас узнать.
Сессии хранятся на сервере и обычно передается в cookie только их идентификатор. И именно этот механизм наиболее часто используется для авторизации, т.е. по сессионным cookie сервер вас идентифицирует, раз он смог это сделать значит вы прошли аутентификацию и может разрешить (или запретить) доступ к ресурсу.
В большинстве фреймворков у вас уже будут готовые модули для регистрации, аутентификации и хранения паролей, авторизации, сессий. И лучше использовать их. Во всех примерах выше показана идея, максимально упрощенная от всех деталей.
Виды токенов
Рассмотрим некоторые виды токенов
- API токен - это токены, выдаваемые сервисов для последующей авторизации. Например, у вас есть приложение которое по запросу отдает данные погоды. Вы не хотите делать его бесплатным для всех, поэтому все кто купил подписку получают токен и в следующий раз при запросе отправляют этот токен. Что-то вроде ключа доступа
- PAT (Personal access token) - такие токены использует например Github. Например, у вас есть репозиторий и вы не хотите чтобы любой человек мог отправлять в него изменения. Но вы можете дать токен (с очень тонкими настройками на время, расположение и т.д.) и позволить конкретному человеку отправлять изменения в репозиторий. Он сможет это сделать только при наличии токена.
- Физические токены - это устройства, которые по определенному алгоритму генерируют токен при подключении (например по USB)
- Токены обновления (refresh tokens) - эти токены нужны не сами по себе, с их помощью можно обновить (т.е. получить новый) токен. Обычно они идут сразу в паре.
- JWT - на нем остановимся поподробнее
JWT
JWT (JSON Web Token) - один из популярных примеров токенов, он может использоваться как для аутентификации, так и просто для передачи дополнительных данных (подробнее).
JWT состоит из трех частей
- заголовок (header) - содержит тип токена и алгоритм шифрования
- полезные данные (payload) - это набор ключ-значение в котором может лежать любые данные
- сигнатура (signature) - криптографический ключ, который можно использовать для подтверждения истинности первых двух частей.
Принцип работы JWT следующий (пример с официального сайта). Пусть у нас имеется следующая структура: в header тип алгоритма и токена, в payload несколько ключей, а signature вычисляется как первые две части + секретная фраза, которую знает только сервер. В итоге мы сформируем некоторый хеш, который подписывает первые две части.

После этого мы возьмем все 3 части, переведем их в base64 и соединим через точку. Получим следующее. Разные части выделены разным цветом.

Но давайте изменим что-нибудь в данных. Изменим значение name с "John Doe" на "John Dou". Да, мы заменили одну букву. Но сигнатура вычисляется на основе первых двух частей + секретный ключ, а это значит что после этого мы получим следующее значение токена.

В поле данных есть одно едва заметное изменение (с ..RG9l.. на ..RG91..), но вот хеш изменился полностью. А это значит любое, даже самое малое изменение не останется незамеченным.
Если сервер отдал токен, а потом ему с запросом пришел какой-то токен и он с помощью секретного ключа смог его расшифровать и получил точно такие же header и payload - он может быть уверен в их верности.
Общий сценарий работы с токенами
В целом можно сказать что все токены ведут себя подобным образом
- Первоначальный запрос - вы запрашиваете некоторый ресурс
- Сервер пытается выяснить, есть ли у вас права на данный ресурс. Скорее всего нет, поэтому сервис предлагает пройти аутентификацию (необязательно по паролю)
- Вы проходите аутентификацию и сервис генерирует вам токен определенного формата
- С каждым последующим запросом вы отсылаете токен
Ассиметричное шифрование
Аутентификация на основе ассиметричного шифрования выглядит следующим образом.
Есть сервер и клиент. У клиента есть секретный (закрытый) ключ, которым можно расшифровать сообщения. На сервере лежит открытый ключ, которым сообщения шифруют. Сервер генерирует случайную строку (или число), шифрует ее открытым ключом и отсылает его клиенту. Клиент, используя закрытый ключ, расшифровывает строку и посылает ее обратно (конечно все немного сложнее, посылаются хеш значения и не в открытом виде, но идея такая - только клиент может расшифровать фразу, т.к. предполагается что только он обладает секретным ключем). Если полученное сообщение на сервере совпадает со сгенерированным - аутентификация прошла успешно.
Что есть еще
Технологии, которые не вошли в статью, но о которых стоит упомянуть
- SSO (single sign-on) - метод аутентификации, который позволяет пользователям безопасно аутентифицироваться сразу в нескольких приложениях и сайтах, используя один набор учетных данных
- OpenID Connect - стандарт, предоставляющей пользователю возможность создать единую учётную запись для аутентификации на множестве не связанных друг с другом интернет-ресурсов
- Kerberos - сетевой протокол аутентификации, предлагающий механизм взаимной аутентификации клиента и сервера
- Хранение паролей и токенов. Важная тема, т.к. потеря и пароля, и токена почти то же самое. Различные программы для хранения паролей работают по следующей логике. Вы вводите пароль для сохранения, этот пароль шифруется и сохраняется на диск. Когда вам нужно его посмотреть - программа достает данные, расшифровывает в оперативной памяти и показывает пароль. Важно чтобы пароль в чистом виде никогда не хранился на диске.
- oAuth 2.0 - протокол авторизации, который позволяет выдать одному приложению права на доступ другому приложению (либо права на какую то информацию внутри приложения)
- - - - - - - - - - - - - - - -
Для тех, кто дочитал до конца
На написание подобных статей уходит много времени и сил, но я стараюсь достать информацию из разных источников и выложить в одной статье все самое ценное и полезное. Поэтому они получаются большими и поэтому выходят не так часто. Если вам нравится подобная подача, вы можете поддержать канал:
- поделиться ссылкой на канал [https://t.me/line_of_code]
- дополнить/уточнить статью в комментариях (я не знаю всего и я могу ошибаться) или предложить интересную тему
- поддержать материально - https://yoomoney.ru/to/4100117706200369