Авторизация по номеру телефона php

Организация аутентификации по СМС по примеру 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.

Если же нет, то варианты исключений:

  1. Некорректный токен: HTTP код состояния: 404.
  2. Некорректный код: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: SMS_CODE_INVALID.
  3. Телефон уже подтверждён: HTTP код состояния: 422 (Unprocessable Entity), в теле ответа: ALREADY_CONFIRMED.

3. Форсированная отправка кода повторно.

 PUT /api/sms_authentications//resend

Аналогично. Если всё ок — код 200.

Если же нет, то варианты исключений:

  1. Некорректный токен: HTTP код состояния: 404.
  2. Слишком частая отправка (скажем, прошлая отправка была не позднее чем 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.

Изменения в схеме БД

Итого, в модели (или в таблице БД, если угодно) надо хранить:

  1. Телефон: phone (советую использовать библиотеки для унификации телефонного номера, вроде этой для Rails),
  2. Ключ для TOTP: otp_secret_key (читаете подробное README для выбранной библиотеки TOTP),
  3. Токен: token (создаете при первом запросе к API чем-нибудь типа SecureRandom),
  4. Ссылку на пользователя: user_id (если у вас есть отдельная таблица/модель, где хранятся данные пользователя).

Особенности реализации мобильного приложения

В случае Android полученный токен можно хранить в SharedPreferences (почему не AccountManager), а для iOS в KeyChain. См. обсуждение на SoF.

Заключение

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

Источник

Оцените статью