Статьи

Многофакторная аутентификация с PHP и Twilio

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

Использование биометрических данных для аутентификации кого-либо на вашем веб-сайте, вероятно, еще далеко, чтобы преодолеть многие технологические проблемы и проблемы гражданских свобод. Более практичный подход к многофакторной аутентификации заключается в реализации опций из двух других категорий, таких как требование пароля и токена подтверждения, отправленного пользователю по телефону с помощью SMS или голосового вызова (пользователь знает пароль и имеет Телефон). Это то, что вы узнаете, как реализовать в этой статье.

Интеграция телефонии может разочаровать, если вы делаете это с нуля, и большую часть времени нецелесообразно создавать собственную инфраструктуру (хотя это возможно с использованием аппаратного обеспечения и программ, таких как Asterisk ). К счастью, Twilio предлагает инфраструктуру и API, которые разработчики могут использовать для написания приложений интерактивной телефонии без особых хлопот. Я буду использовать их услуги в этой статье.

Вы можете совершать и принимать телефонные звонки, отправлять и получать текстовые сообщения с помощью Twilio, используя TwiML (язык разметки Twilio) и их REST API. Вы можете работать напрямую с API или использовать одну из доступных вспомогательных библиотек . Здесь я буду использовать библиотеку twilio-php , библиотеку, выпущенную и официально поддерживаемую Twilio.

Итак, вы готовы узнать, как можно реализовать многофакторную аутентификацию? Читай дальше!

Использование Twilio из PHP

Подключиться к сервису Twilio так же просто, как включить библиотеку twilio-php и создать новый экземпляр класса Services_Twilio . Конструктор объекта принимает SID и токен вашей учетной записи, которые перечислены на странице панели вашей учетной записи Twilio после того, как вы зарегистрируетесь в их службе.

Имея в своем распоряжении доступный экземпляр Services_Twilio , вы можете получить доступ к их API через свойство учетной записи. учетная запись предоставляет экземпляр Services_Twilio_Rest_Account который представляет вашу учетную запись Twilio. Здесь я использую только одну учетную запись, но возможно иметь несколько дополнительных учетных записей. Это может быть полезно для сегментирования ваших взаимодействий в зависимости от ваших потребностей. Вы можете узнать больше о субсчетах , прочитав документацию Twilio.

 <?php require "Services/Twilio.php"; define("TWILIO_SID", "…"); define("TWILIO_AUTH_TOKEN", "…"); $twilio = new Services_Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN); $acct = $twilio->account; 

Экземпляр учетной записи предоставляет несколько других свойств, среди которых вызовы и sms_messages. Это экземпляры таких объектов, как Services_Twilio_Rest_Calls и Services_Twilio_Rest_SmsMessages которые инкапсулируют ресурсы REST, используемые для отправки вызовов и сообщений соответственно. Тем не менее, вы редко работаете с этими объектами за пределами их методов create() , и документация ссылается на свойства, которые представляют их как «ресурсы экземпляра». Я сделаю то же самое, чтобы избежать путаницы.

Отправка смс сообщения

Отправка SMS-сообщения осуществляется с помощью метода create() ресурса экземпляра SMS-сообщения ( $acct->sms_messages ). Этот метод принимает три аргумента: номер Twilio в вашей учетной записи (сродни «от адреса» электронной почты), номер получателя («на адрес») и текст вашего сообщения (которое может содержать до 160 символов). ).

 <?php $code = "1234"; // some random auth code $msg = $acct->sms_messages->create( "+19585550199", // "from" number "+19588675309", // "to" number "Your access code is " . $code ); 

За кулисами библиотека twilio-php отправляет POST-запрос некоторого TwiML от вашего имени в API Twilio. Экземпляр Services_Twilio_Rest_SmsMessage возвращается вызовом, который инкапсулирует информацию о сообщении. Вы можете увидеть полный список информации, доступной в документации , но, возможно, более важные значения отображаются в свойствах status и price . status показывает состояние SMS-сообщения (в очереди, отправке, отправке или отказе), а price показывает сумму, выставленную вашей учетной записи за сообщение.

Отправка голосового вызова

Инициирование голосового вызова осуществляется с помощью метода create() ресурса экземпляра Calls ( $acct->calls ). Как и при отправке SMS-сообщений, вам необходимо указать номер своей учетной записи, номер получателя и сообщение. Однако в этом случае сообщение представляет собой URL-адрес документа TwiML, в котором описывается характер вызова.

 <?php // Note spaces between each letter in auth code. This forces // the speech synthesizer to enunciate each digit. $code = "1 2 3 4"; $msg = $acct->calls->create( "+19585550199", // "from" number "+19588675309", // "to" number "http://example.com/voiceCall.php?code=" . urlencode($code) ); 

Снова библиотека отправляет запрос POST от вашего имени, и выполняется голосовой вызов. Когда вызываемый абонент отвечает на телефонный звонок, процессы Twilio получают и выполняют команды, указанные в XML-адресе обратного вызова. В приведенном выше примере, который инициирует голосовой вызов, я передал код подтверждения в качестве параметра GET в URL-адресе скрипту обратного вызова. Когда Twilio получает URL-адрес, PHP будет использовать этот параметр при отображении ответа.

Существует только несколько тегов TwiML, которые необходимы для построения потока вызовов, но вы можете использовать их для определения довольно сложных потоков (таких как меню дерева телефонов и т. Д.). Основной поток вызовов для этого типа сценария может быть сгенерирован PHP и выглядеть примерно так:

 <?php header("Content-Type: text/xml"); $code = isset($_GET["code"]) ? htmlspecialchars($_GET["code"]) : null; $digit = isset($_POST["Digits"]) ? (int)$_POST["Digits"] : null; $url = htmlspecialchars($_SERVER["PHP_SELF"]) . "?code=" . $code; echo '<?xml version="1.0" encoding="UTF-8"?>'; ?> <Response> <?php if (is_null($code)) { ?> <Say>Sorry. An error occurred.</Say> <?php } else { ?> <Gather action="<?php echo $url; ?>" numDigits="1"> <?php if (is_null($digit) || $digit == 1) { ?> <Say>Your access code is <?php echo $code; ?></Say> <?php } ?> <Say>Press 1 to repeat the code.</Say> </Gather> <?php } ?> <Say>Good bye.</Say> </Response> 

Здесь используются следующие теги TwiML: Response (корневой элемент), Say (предоставляет текст, на котором будет говорить Twilio) и Gather (собирает данные от пользователя).

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

Документы Twilio для Gather очень хорошо объясняют поведение и атрибуты элементов, которые вы можете использовать для изменения поведения, и даже перечисляют пару примеров. Я рекомендую вам прочитать его.

Есть еще одно замечание, прежде чем двигаться дальше; Я добавил пробелы между каждой цифрой в коде авторизации в исходном скрипте. Это заставляет синтезатор речи произносить каждую цифру, говоря «один два три четыре» вместо «одна тысяча двести тридцать четыре». При синтезе речи иногда то, что мы видим в тексте, не всегда то, что мы получаем. Это нормально, чтобы выдумывать или неправильно писать голосовой диалог, если это приводит к лучшей ясности и пониманию для ваших собеседников.

Реализация многофакторной аутентификации

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

Пользователю должна быть предоставлена ​​форма входа в систему, которая инициирует процесс. Отправка формы приводит вас к проверке ее учетных данных и отклонению их, если они неверны, но действительные учетные данные не должны немедленно аутентифицировать пользователя в вашем приложении. Вместо этого, рассмотрите пользователя как «частично аутентифицированного», состояние, которое позволяет ему просматривать вторую форму, запрашивающую код, который был отправлен на ее телефон. Только после того, как она отправит правильный код, пользователь должен быть авторизован.

 <?php session_start(); require "Services/Twilio.php"; define("TWILIO_SID", "…"); define("TWILIO_AUTH_TOKEN", "…"); define("TWILIO_FROM_NUMBER", "+19585550199"); define("TWILIO_VOICE_URL", "http://example.com/voiceCall.php"); define("DB_HOST", "localhost"); define("DB_DBNAME", "test"); define("DB_USER", "dbuser"); define("DB_PASSWORD", "dbpassword"); define("CRYPT_SALT", '$2a$07$R.gJb2U2N.FmZ4hPp1y2CN'); define("AUTH_CODE_LENGTH", 4); $db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_DBNAME, DB_USER, DB_PASSWORD); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if ($_SERVER["REQUEST_METHOD"] == "POST") { // first stage of authentication if (empty($_SESSION["username"])) { $username = isset($_POST["username"]) ? $_POST["username"] : ""; $password = isset($_POST["password"]) ? crypt($_POST["password"], CRYPT_SALT) : ""; $query = sprintf("SELECT username, phone_number, code_pref FROM users WHERE username = %s AND password = %s", $db->quote($username), $db->quote($password)); $result = $db->query($query); $row = $result->fetch(PDO::FETCH_ASSOC); $result->closeCursor(); // invalid username/password provided if (!$row) { session_unset(); } // valid username/password else { $_SESSION["isAuthenticated"] = false; $_SESSION["username"] = $row["username"]; // generate and send auth code $code = ""; for ($i = 0; $i < AUTH_CODE_LENGTH; $i++) { $code .= rand(0, 9); } $twilio = new Services_Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN); $acct = $twilio->account; // send code via voice or SMS depending on // the user's preference if ($row["code_pref"] == "voice") { // add spaces to force enunciation $tmpCode = join(" ", string_split($code)); $msg = $acct->calls->create( TWILIO_FROM_NUMBER, $_row["phone_number"], TWILIO_VOICE_URL . "?code=" . urlencode($tmpCode) ); } else { $msg = $acct->sms_messages->create( TWILIO_FROM_NUMBER, $_row["phone_number"], "Your access code is " . $code ); } // "remember" code in session $_SESSION["code"] = $code; } } // second stage authentication else { $code = isset($_POST["code"]) ? $_POST["code"] : ""; if ($code == $_SESSION["code"]) { $_SESSION["isAuthenticated"] = true; unset($_SESSION["code"]); } } } if (!empty($_SESSION["isAuthenticated"])) { ?> <h1>W00t! You're Authenticated!</h1> <?php } // present login forms else { if (empty($_SESSION["username"])) { ?> <h1>Login Step 1</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="Login"> </form> <?php } else { ?> <h1>Login Step 2</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="code"> <input type="submit" value="Confirm"> </form> <?php } } с <?php session_start(); require "Services/Twilio.php"; define("TWILIO_SID", "…"); define("TWILIO_AUTH_TOKEN", "…"); define("TWILIO_FROM_NUMBER", "+19585550199"); define("TWILIO_VOICE_URL", "http://example.com/voiceCall.php"); define("DB_HOST", "localhost"); define("DB_DBNAME", "test"); define("DB_USER", "dbuser"); define("DB_PASSWORD", "dbpassword"); define("CRYPT_SALT", '$2a$07$R.gJb2U2N.FmZ4hPp1y2CN'); define("AUTH_CODE_LENGTH", 4); $db = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_DBNAME, DB_USER, DB_PASSWORD); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if ($_SERVER["REQUEST_METHOD"] == "POST") { // first stage of authentication if (empty($_SESSION["username"])) { $username = isset($_POST["username"]) ? $_POST["username"] : ""; $password = isset($_POST["password"]) ? crypt($_POST["password"], CRYPT_SALT) : ""; $query = sprintf("SELECT username, phone_number, code_pref FROM users WHERE username = %s AND password = %s", $db->quote($username), $db->quote($password)); $result = $db->query($query); $row = $result->fetch(PDO::FETCH_ASSOC); $result->closeCursor(); // invalid username/password provided if (!$row) { session_unset(); } // valid username/password else { $_SESSION["isAuthenticated"] = false; $_SESSION["username"] = $row["username"]; // generate and send auth code $code = ""; for ($i = 0; $i < AUTH_CODE_LENGTH; $i++) { $code .= rand(0, 9); } $twilio = new Services_Twilio(TWILIO_SID, TWILIO_AUTH_TOKEN); $acct = $twilio->account; // send code via voice or SMS depending on // the user's preference if ($row["code_pref"] == "voice") { // add spaces to force enunciation $tmpCode = join(" ", string_split($code)); $msg = $acct->calls->create( TWILIO_FROM_NUMBER, $_row["phone_number"], TWILIO_VOICE_URL . "?code=" . urlencode($tmpCode) ); } else { $msg = $acct->sms_messages->create( TWILIO_FROM_NUMBER, $_row["phone_number"], "Your access code is " . $code ); } // "remember" code in session $_SESSION["code"] = $code; } } // second stage authentication else { $code = isset($_POST["code"]) ? $_POST["code"] : ""; if ($code == $_SESSION["code"]) { $_SESSION["isAuthenticated"] = true; unset($_SESSION["code"]); } } } if (!empty($_SESSION["isAuthenticated"])) { ?> <h1>W00t! You're Authenticated!</h1> <?php } // present login forms else { if (empty($_SESSION["username"])) { ?> <h1>Login Step 1</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="username"> <input type="password" name="password"> <input type="submit" value="Login"> </form> <?php } else { ?> <h1>Login Step 2</h1> <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post"> <input type="text" name="code"> <input type="submit" value="Confirm"> </form> <?php } } 

Приведенный выше код предназначен только для иллюстрации. В вашем реальном приложении вы можете добавить следующее:

  • Добавьте ссылку для повторной отправки кода подтверждения на телефон пользователя.
  • Добавьте ссылку отмены в форму запроса кода на случай, если пользователь решит не продолжать процесс. Что касается приведенного выше кода, такая ссылка должна была бы сбрасывать $_SESSION["username"] поскольку значение, помимо хранения имени пользователя, также действует как флаг «частичной аутентификации» относительно $_SESSION["isAuthenticated"] .
  • Добавьте регулирование или блокировку учетной записи, если указан неправильный код.
  • В зависимости от вашего уровня паранойи или требований соответствия, с которыми вы сталкиваетесь, регистрируйте события аутентификации где-нибудь (Приложение, которое требует многофакторной аутентификации, обычно достаточно чувствительно, чтобы гарантировать контрольный журнал!)

Кроме того, вы можете подумать о создании достаточно сложных кодов аутентификации для ваших целей. Четырехзначные числовые коды довольно распространены, но вы не ограничены этим. Если вы решите использовать буквы или сочетание буквенно-цифровых значений, я бы рекомендовал избегать значений, которые можно легко перепутать (например, число 0 против буквы O, число 1 против буквы I и т. Д.).

Резюме

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

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

Изображение через Fotolia