- Организация аутентификации по СМС по примеру Telegram/Viber/WhatsApp
- Изменения в API
- 1. Запросить СМС с кодом на номер, в ответ — токен для последующих действий.
- 2. Подтвердить токен с помощью кода из СМС.
- 3. Форсированная отправка кода повторно.
- Особенности реализации одноразовых паролей
- Безопасность решения
- Изменения в схеме БД
- Особенности реализации мобильного приложения
- Заключение
Организация аутентификации по СМС по примеру Telegram/Viber/WhatsApp
Представим, что перед вами стоит задача организовать аутентификацию пользователя (в мобильном приложении, в первую очередь) так, как это сделано в Telegram/Viber/WhatsApp. А именно реализовать в API возможность осуществить следующие шаги:
- Пользователь вводит свой номер телефона и ему на телефон приходит СМС с кодом.
- Пользователь вводит код из СМС и приложение его аутентифицирует и авторизует.
- Пользователь открывает приложение повторно, и он уже аутентифицирован и авторизован.
Мне потребовалось некоторое количество времени, чтобы осознать, как правильно это сделать. Моя задача — поделиться наработанным с вами в надежде, что это сэкономит кому-то времени.
Я постараюсь кратко изложить выработанный подход к этому вопросу. Подразумевается, что у вас API, HTTPS и, вероятно, REST. Какой у вас там набор остальных технологий неважно. Если интересно — добро пожаловать под кат.
Мы поговорим о тех изменениях, которые следует проделать в API, о том, как реализовать одноразовые пароли на сервере, как обеспечить безопасность (в т.ч. защиту от перебора) и в какую сторону смотреть при реализации это функциональности на мобильном клиенте.
Изменения в API
В сущности требуется добавить три метода в ваше API:
1. Запросить СМС с кодом на номер, в ответ — токен для последующих действий.
Действие соответствует CREATE в CRUD.
POST /api/sms_authentications/ Параметры на вход: phone Параметры на выход: token
Если всё прошло, как ожидается, возвращаем код состояния 200.
Если же нет, то есть одно разумное исключение (помимо стандартной 500 ошибки при проблемах на сервере и т.п. — некорректно указан телефон. В этом случае:
HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: PHONE_NUMBER_INVALID.
2. Подтвердить токен с помощью кода из СМС.
Действие соответствует UPDATE в CRUD.
PUT /api/sms_authentications// Параметры на вход: sms_code
Аналогично. Если всё ок — код 200.
Если же нет, то варианты исключений:
- Некорректный токен: HTTP код состояния: 404.
- Некорректный код: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: SMS_CODE_INVALID.
- Телефон уже подтверждён: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: ALREADY_CONFIRMED.
3. Форсированная отправка кода повторно.
PUT /api/sms_authentications//resend
Аналогично. Если всё ок — код 200.
Если же нет, то варианты исключений:
- Некорректный токен: HTTP код состояния: 404.
- Слишком частая отправка (скажем, прошлая отправка была не позднее чем 60 секунд назад): HTTP код состояния: 400 (BAD_REQUEST), в теле ответа: TOO_OFTEN.
Помимо этого, каждый метод API, который требует аутентифицированного пользователя должен получать на вход дополнительный параметр token , который связан с пользователем.
Особенности реализации одноразовых паролей
Вам потребуется хранить специальный ключ для проверки СМС-кодов. Существует алгоритм TOTP, который, цитирую Википедию:
OATH-алгоритм создания одноразовых паролей для защищенной аутентификации, являющийся улучшением HOTP (HMAC-Based One-Time Password Algorithm). Является алгоритмом односторонней аутентификации — сервер удостоверяется в подлинности клиента. Главное отличие TOTP от HOTP это генерация пароля на основе времени, то есть время является параметром[1]. При этом обычно используется не точное указание времени, а текущий интервал с установленными заранее границами (например, 30 секунд).
Грубо говоря, алгоритм позволяет создать одноразовый пароль, отправить его в СМС, и проверить, что присланный пароль верен. Причём сгенерированный пароль будет работать заданное количество времени. При всём при этом не надо хранить эти бесконечные одноразовые пароли и время, когда они будут просрочены, всё это уже заложено в алгоритм и вы храните только ключ.
Пример кода на руби, чтобы было понятно о чём речь:
totp = ROTP::TOTP.new("base32secret3232") totp.now # => "492039" # OTP verified for current time totp.verify("492039") # => true sleep 30 totp.verify("492039") # => false
Алгоритм описан в стандарте RFC6238, и существует масса реализацией этого алгоритма для многих языков: для Ruby и Rails, для Python, для PHP и т.д..
Строго говоря, Telegram и компания не используют TOTP, т.к. при регистрации там, вас не ограничивают по времени 30-ю секундами. В связи с этим предлагается рассмотреть альтернативный алгоритм OTP, который выдает разные пароли, базируясь на неком счётчике, но не на времени. Встречаем, HOTP:
HOTP (HMAC-Based One-Time Password Algorithm) — алгоритм защищенной аутентификации с использованием одноразового пароля (One Time Password, OTP). Основан на HMAC (SHA-1). Является алгоритмом односторонней аутентификации, а именно: сервер производит аутентификацию клиента.
…
HOTP генерирует ключ на основе разделяемого секрета и не зависящего от времени счетчика.
HOTP описан в стандарте RFC4226 и поддерживается тем же набором библиотек, что представлен выше. Пример кода на руби:
hotp = ROTP::HOTP.new("base32secretkey3232") hotp.at(0) # => "260182" hotp.at(1) # => "055283" hotp.at(1401) # => "316439" # OTP verified with a counter hotp.verify("316439", 1401) # => true hotp.verify("316439", 1402) # => false
Безопасность решения
Первое непреложное само собой разумеющееся правило: ваше API, где туда-сюда гуляют данные и, самое главное, token должно быть завернуто в SSL. Поэтому только HTTPS, никакого HTTP.
Далее, самым очевидным вектором атаки является прямой перебор. Вот что пишут в параграфе 7.3 авторы стандарта HOTP (на котором базируется TOTP) на эту тему:
Truncating the HMAC-SHA-1 value to a shorter value makes a brute force attack possible. Therefore, the authentication server needs to detect and stop brute force attacks.
We RECOMMEND setting a throttling parameter T, which defines the maximum number of possible attempts for One-Time Password validation. The validation server manages individual counters per HOTP device in order to take note of any failed attempt. We RECOMMEND T not to be too large, particularly if the resynchronization method used on the server is window-based, and the window size is large. T SHOULD be set as low as possible, while still ensuring that usability is not significantly impacted.
Another option would be to implement a delay scheme to avoid a brute force attack. After each failed attempt A, the authentication server would wait for an increased T*A number of seconds, e.g., say T = 5, then after 1 attempt, the server waits for 5 seconds, at the second failed attempt, it waits for 5*2 = 10 seconds, etc.
The delay or lockout schemes MUST be across login sessions to prevent attacks based on multiple parallel guessing techniques.
Если кратко, то от прямого перебора алгоритм априори не защищает и надо такие вещи предотвращать на уровне сервера. Авторы предлагают несколько решений:
- Отслеживать число неудачных попыток ввода кода, и блокировать возможность аутентификации по превышению некоторого максимального лимита. Лимит предлагают делать настолько маленьким, насколько ещё будет комфортно пользоваться сервисом.
- Установить задержку после неудачной попытки ввода. Причём увеличивать задержку линейно по числу неудачных попыток. К примеру, после первой попытки — установить задержку в 5 секунд, после второй в 10 и т.п..
Мнение, что можно полагаться только на то, что код живёт ограниченное число секунд, и будет безопасно, т.к. код сбрасывается — ошибочно. Даже, если есть фиксированное ограничение на число попыток в секунду.
Посмотрим на примере. Пусть код TOTP состоит из 6 цифр — это 1000000 возможных вариантов. И пусть разрешено вводить 1 код в 1 секунду, а код живёт 30 секунд.
Шанс, что за 30 попыток в 30 секунд будет угадан код — 3/100000 ~ 0.003%. Казалось бы мало. Однако, таких 30-ти секундных окон в сутках — 2880 штук. Итого, у нас вероятность угадать код (даже несмотря на то, что он меняется) = 1 — (1 — 3/100000)^2880 ~ 8.2%. 10 дней таких попыток уже дают 57.8% успеха. 28 дней — 91% успеха.
Так что надо чётко осознавать, что необходимо реализовать хотя бы одну (а лучше обе) меры, предложенные авторами стандарта.
Не стоит забывать и о стойкости ключа. Авторы в параграфе 4 обязывают длину ключа быть не менее 128 бит, а рекомендованную длину устанавливают в 160 бит (на данный момент неатакуемая длина ключа).
R6 — The algorithm MUST use a strong shared secret. The length of the shared secret MUST be at least 128 bits. This document RECOMMENDs a shared secret length of 160 bits.
Изменения в схеме БД
Итого, в модели (или в таблице БД, если угодно) надо хранить:
- Телефон: phone (советую использовать библиотеки для унификации телефонного номера, вроде этой для Rails),
- Ключ для TOTP: otp_secret_key (читаете подробное README для выбранной библиотеки TOTP),
- Токен: token (создаете при первом запросе к API чем-нибудь типа SecureRandom),
- Ссылку на пользователя: user_id (если у вас есть отдельная таблица/модель, где хранятся данные пользователя).
Особенности реализации мобильного приложения
В случае Android полученный токен можно хранить в SharedPreferences (почему не AccountManager), а для iOS в KeyChain. См. обсуждение на SoF.
Заключение
Вышеописанный подход позволит вам в рамках вашего стека технологий реализовать указанную задачу. Если вас есть соображения по этому подходу или альтернативные подходы, то прошу поделиться в комментариях. Аналогичная просьба, если у вас есть примеры документации к безопасным