Организация аутентификации по СМС по примеру 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/<token>/
        Параметры на вход:
            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/<token>/resend

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


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


    1. Некорректный токен: HTTP код состояния: 404.
    2. Слишком частая отправка (скажем, прошлая отправка была не позднее чем 60 секунд назад): HTTP код состояния: 400 (BAD_REQUEST), в теле ответа: TOO_OFTEN.

    Помимо этого, каждый метод API, который требует аутентифицированного пользователя должен получать на вход дополнительный параметр token, который связан с пользователем.


    Литература:


    1. Образец для подражания — API Telegram: https://core.telegram.org/methods
    2. Дискуссия на SOF: http://stackoverflow.com/questions/12401255/sms-registration-like-in-the-mobile-app-whatsapp

    Особенности реализации одноразовых паролей


    Вам потребуется хранить специальный ключ для проверки СМС-кодов. Существует алгоритм 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.


    Заключение


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

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 25

      +2
      Это точно 1в1 как в Telegram/Viber/WhatsApp? Просто если нет, то зачем?
        0

        Конечно, не точно. Насколько мне известно Telegram/Viber/WhatsApp не раскрывают особенностей серверной архитектуры в этой части. Если это не так — прошу меня поправить.


        У статьи, грубо говоря, следующий посыл: если вы хотите организовать нечто похожее, но не знаете как — то вам сюда.


        P.S. Немножко дополнил введение и в разделе про OTP добавил описание HOTP — это больше похоже на то, что используется в Telegram.

        +3
        Самое главное что много кто пропускает это защита от brute force. А то можно быстро числа угадать
          +1
          Еще бы защиту от многократных запросов.
            0

            Это вроде как и есть brute force или вы что-то ещё имели ввиду?

              0
              Другое — много запросов СМС = дорого.
                0

                Это хороший вопрос. Можно ограничить число СМС в час на определённый номер. Впрочем, ничто не мешает указать произвольное число номеров. Тогда можно банить IP, но ничто не мешает менять IP. В этом случае, можно блокировать регистрацию на время, но, судя по всему, если вас захотят задидосить таким образом — задидосят.


                У вас есть соображения на тему?

            0

            Спасибо, что затронули эту тему. Обновил статью, и добавил раздел Безопасность, где дан ответ на тему противостояние прямому перебору. Перечислил два подхода, которые рекомендуются авторами стандарта.

              0
              Сейчас у себя делаю похожего формата авторизацию. Для защиты от брутфорса отлично помогает два простых правила:

              1) на подтверждение кода дается 30 секунд. Дальше он истечет и нужно будет отправлять новый.
              2) попытка подтвердить код в независимости от исхода этот код удалит отовсюду.
              То есть подобрать можно только алгоритм.

              Итого — берем пользовательские данные, время сервера и набор рандомных байт, делаем хеш, отображаем на числа 0..999999 (а можно и так — пусть пользователи помучаются) произвольным способом, и вуаля.
              0
              Поясните, как выглядит брут-форс атака на TOTP? Если код меняется в течение каждых 60 секунд, и секрет (в основе кода) не скомпрометирован, то достаточно ограничить число попыток ввода.

              Алгоритм простой: человек ошибся с вводом кода, всё, надо ждать следующего. Натуральным образом (при ttl 30с) это даёт всего 2880 попыток в сутки. А дальше простой алгоритм, который увеличивает задержку после каждой следующей попытки. Очевидно, что человек не будет пробовать больше пары тысяч раз ни при каких обстоятельствах (тем паче, код надо не «вспоминать», а «набрать»), то есть увеличение задержки можно делать после пятой-шестой попытки.

                +2
                и еще добавлю

                Ну вот, я уже обрадовался было, что наконец-то Яндекс сделает двухфакторку по RFC, и я смогу его занести в свой MS Authenticator в тёплую компанию к
                Гуглу,
                Майкрософту,
                Гитхабу,
                Дропбоксу,
                Фэйсбуку,
                Вордпрессу,
                Вконтакту,
                и даже моему любимому Тайнипэссу, для которой я сам буквально пару месяцев назад её и реализовал (обкатывается на QA, скоро глобально включим).

                Но нет. Оказывается, Яндекс изобрёл свой собственный нестандартный велосипед, для которого вдобавок нету приложения под WP.

                Спасибо, Яндекс.
                  +1
                  Что есть, то есть, видимо чтобы вот так вот не совали его в MS Authenticator.
                  0

                  Да, вы правы насчёт ограничения или задержки. Просто важно об этом помнить. Я обновил статью и добавил раздел "Безопасность". Процитирую кусочек:


                  Посмотрим на примере. Пусть код 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% успеха.

                  +1
                  В Android'е есть AccountManager, в котором можно хранить токен для пользователя. Чем хранение токена в SharedPreferences лучше?
                    0

                    Вот тут и тут дискуссии на эту тему. Краткий вывод такой:


                    The only reason I could think of that would encourage the use of AccountManager would be if you want to share your account across a number of different apps, as the data is stored in the central Android datastore which can be accessed by all apps. However, if this isn't required, I think its probably easier and simpler to just use SharedPreferences

                    Т.е. если вам не нужно ваши данные аутентификации синхронизировать между многими приложениями, то да — Account Manager, если нет (что значительно чаще. если вы не Яндекс, Google и т.п.), то проще обойтись SharedPreferences.

                      0
                      AccountManager — отвратительный компонент android фреймворка с вырвиглазным API, который просто невозможно уложить на хоть сколько-нибудь вменяемую архитектуру. Плюс из отсутствующих бонусов — в нем нет ничего для шифрования данных. Это нужно делать самому. SharedPreferences проще при таком же выхлопе. Если вдруг нужно работать с одним аккаунтом из нескольких приложений — можно сделать IPC сервис для авторизации.
                      0
                      Считаю аутентификацию исключительно по SMS несекьюрной в принципе. Телефония — штука небезопасная, и любой субъект, имеющий возможность перехвата SMS, может с легкостью получить доступ к вашим данным. Двухфакторная — это другое дело.
                        0

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


                        Тут корректнее говорить, о вероятности и сложности получения доступа. И тут вы, безусловно, правы. Двухфакторка значительно надёжднее. Но она — некоторый компромисс с удобством пользователя, и упомянутые Telegram/Viber/WhatsApp на него не идут, оставляя только СМС. За что, кстати, Телеграм уже платился.


                        Но, если у вас не такое критичное и важное приложение как мессенджер или инернет-банк, то, в целом, можно и пойти на этот риск. Конечно, если ценность ваших конкретных данных (к примеру, в приложениях такси) не сопоставима с затратами на перехват СМС.

                          0
                          Однако если я позволил установить себе кейлоггер, то это мой промах. В случае же, когда мой оператор сливает данные спецслужбам и т.п. — тут никакие превентивные меры мне не помогут. Поэтому сравнение некорректно. Более того, классическая аутентификация по паролю в данном случае даже более безопасна, чем аутентификация исключительно по SMS.
                            0

                            Строго говоря, пока у вас нет 100% контроля на устройством (как его нет ни в случае iOS, ни в случае Android), говорить о том, что это чисто ваш промах не до конца корректно. Теоретически, кейлогер могут установить на заводе, в магазине или где-то в недрах Apple, или Google или Huawei и ему подобные. Кроме того существуют уязвимости нулевого дня, и опять же ваша вина будет только в том, что вы оказались в сети.


                            Превентивные меры в вышеописанных ситуациях тоже неясно, какие можно предпринять.

                              0
                              Вы сравниваете технические возможности организации перехвата моих данных с организационными. Это некорректно. Одно дело случайный взлом по причине совпадения звезд и другое дело — целенаправленный перехват данных конкретного абонента.
                                0

                                В целом, я комментировал фразу про то, чей промах, и помогут ли превентивные меры.


                                Кроме того, что касается Apple. Если им не западло удалять "пиратские" файлы с компьютеров пользователей, или Google не брезгует объявлять, что Android будет удалять пиратское ПО с телефонов пользователей, то я не вижу технических причин у этих компаний не перехватить ваши пароли. Только организационные.


                                У вас нет контроля над их ПО. Вы точно также доверяете свои данные их ПО, как вы доверяете свою переписку своему ОПСОСу.


                                Может показаться, что МТС себя скомпрометировало и попалось на этом, а Google и Apple — уважаемые западные компании и не позволяют себе такого. Так вот, согласно Guardian, это не так.


                                Ведущие американские ИТ-компании знали о том, что АНБ собирает данные их пользователей, и даже помогали агентству. Об этом сообщил главный юрист АНБ.



                                Он сообщил, что АНБ получала как содержимое сообщений пользователей, которые они пересылали друг-другу, так и сопутствующие метаданные.
                          0
                          А никто ведь и не говорит, что этот метод используется 'as is'. Его можно абсолютно так же обобщить на двухфакторную авторизацию/аутентификацию.
                          0
                          У себя в маленьком проектике сделал довольно банальную генерацию кодов из 6 символов (самые банальные хеши с участием времени и пользовательской информации) и, чтобы избежать сложных алгоритмов просто закидывал их в Redis с фиксированным expiration. ИМХО, вышло удобнее, чем пропихивать сложную криптографию в проект ;) Да и на первый взгляд кажется вполне безопасно.
                            0
                            У Вас в тексте есть токены, ключи, коды. Для чего такая сложность? юзер делает запрос, генерим ему код из N символов, отправляем этот код на номер и сохраняем в табличку (дата обновления — юзер_ид — код — счетчик). Юзер делает попытку, смотрим на время обновления кода в таблице, если проходим по условиям — пропускаем юзера, если нет — добавляем счетчик в табличке, при наборе определённого числа блокируем юзера на какое-то время.

                            Only users with full accounts can post comments. Log in, please.