Статьи

PHP авторизация с JWT (JSON Web Tokens)

Если вам нравятся темы компьютерной безопасности, вы будете знать, что одной из самых обсуждаемых и противоречивых тем является аутентификация пользователя. В его контексте вы найдете широкий спектр областей исследования, от новых механизмов до юзабилити. Таким образом, к моему удивлению, JSON Web Tokens — тема, о которой не часто говорят , и я думаю, что она заслуживает того, чтобы быть в центре внимания сегодня. Мы увидим, как легко интегрировать его в механизм аутентификации API.

Key icon

Против сессий

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

Некоторые из недостатков выдачи учетных данных приложению и поддержания состояния пользователя по отношению к приложению с использованием сеансовых файлов cookie:

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

  • Файловая система запросов на чтение / запись
    Каждый раз, когда начинается сеанс или изменяются его данные, серверу необходимо обновить файл сеанса. То же самое касается каждого раза, когда приложение отправляет куки-файл сессии. Если у вас будет значительное количество пользователей, у вас будет медленный сервер, если вы не используете альтернативные хранилища сеансов.

  • Распределенные / кластерные приложения
    Поскольку файлы сеансов по умолчанию хранятся в файловой системе, трудно иметь распределенную или кластеризованную инфраструктуру для приложений высокой доступности, для которых требуется использование балансировщиков нагрузки, кластерных серверов и т. Д. Необходимо выполнить другие носители и специальные конфигурации. ,

При работе с сервисными API, которые имеют ограниченные сервисные вызовы, вам нужно будет добавлять свой ключ к каждому выполненному запросу (либо в заголовке запроса, таком как Authorization , либо в строке запроса URL). Ключи API обычно используют централизованный механизм управления ими. Поэтому, если вы хотите пометить ключ API как недействительный, его следует отозвать на стороне приложения.

JWT

С октября 2010 года было несколько предложений по использованию токенов на основе JSON . JWT или JSON Web Token были предложены в декабре 2010 года и имели следующие характеристики:

  • Предназначен для сред с ограниченным пространством, таких как заголовки авторизации HTTP или параметры строки запроса.
  • Данные для передачи в формате нотации объектов Javascript (JSON)
  • Данные должны быть полезными данными JSON Web Signature (JWS)
  • Представлено с использованием URL-кодирования Base64

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

Использование JWT имеет много преимуществ перед одним ключом API:

  • Ключи API — это просто случайные строки, в то время как JWT содержат информацию и метаданные, которые могут описывать личность пользователя, данные авторизации и действительность токена в пределах временного интервала или домена.
  • JWT не требуют централизованного выдачи или отзыва полномочий.
  • OAUTH2 совместимый.
  • Данные JWT могут быть проверены.
  • У JWT есть контроль истечения.

19 мая 2015 года JWT стал опубликованным IETF RFC 7519 .

На что это похоже?

JWT будет выглядеть следующим образом:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E 

Может показаться, что строка представляет собой просто случайные символы, соединенные вместе, и не сильно отличается от ключа API. Однако, если вы посмотрите внимательно, на самом деле есть 3 строки, разделенные точечным символом.

Первая и вторая строки — строки JSON в кодировке Base64 URL, поэтому, если мы их декодируем, мы получим следующие результаты:

 { "alg": "HS256", "typ": "JWT" } 
 { "iat": 1416929109, "jti": "aa7f8d0a95c", "scopes": [ "repo", "public_repo" ] } 

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

Что интересно в сигнатуре, так это то, что криптографическому алгоритму требуется секретный ключ, строка, которую должен знать только приложение-эмитент и никогда не должен раскрываться. Таким образом, когда приложение получает токен, оно может сверять подпись с содержимым токена, используя указанный секретный ключ. Если проверка подписи не удалась, мы можем точно знать, что данные внутри токена были подделаны и должны быть отброшены.

Вы можете взглянуть на jwt.io, где вы можете поиграть с кодированием и декодированием JWT.

Поиграем

Итак, как мы можем применить это к приложению PHP? Допустим, у нас есть механизм входа в систему, который в настоящее время использует файлы cookie сеанса для хранения информации о состоянии входа пользователя в приложение. Обратите внимание, что JWT не был предназначен для замены файлов cookie сеанса . Однако для этого примера у нас будет пара сервисов: один, который генерирует JWT на основе предоставленных имени пользователя и пароля, и другой, который будет выбирать защищенный ресурс, если мы предоставим действительный JWT.

Как только мы войдем в систему, мы сможем извлечь защищенный ресурс из приложения.

Для начала мы установим php-jwt с помощью composer require firebase/php-jwt . В примере приложения, разработанного для этого учебника, я также использую zend-config и zend-http, поэтому, если вы хотите следовать, не стесняйтесь установить их также:

 composer require firebase/php-jwt:dev-master composer require zendframework/zend-config:~2.3 composer require zendframework/zend-http:~2.3 

Есть еще одна PHP-библиотека, jose from namshi, если вы захотите поиграть с ней позже.

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

 <?php require_once('vendor/autoload.php'); /* * Application setup, database connection, data sanitization and user * validation routines are here. */ $config = Factory::fromFile('config/config.php', true); // Create a Zend Config Object if ($credentialsAreValid) { $tokenId = base64_encode(mcrypt_create_iv(32)); $issuedAt = time(); $notBefore = $issuedAt + 10; //Adding 10 seconds $expire = $notBefore + 60; // Adding 60 seconds $serverName = $config->get('serverName'); // Retrieve the server name from config file /* * Create the token as an array */ $data = [ 'iat' => $issuedAt, // Issued at: time when the token was generated 'jti' => $tokenId, // Json Token Id: an unique identifier for the token 'iss' => $serverName, // Issuer 'nbf' => $notBefore, // Not before 'exp' => $expire, // Expire 'data' => [ // Data related to the signer user 'userId' => $rs['id'], // userid from the users table 'userName' => $username, // User name ] ]; /* * More code here... */ } 

Обратите внимание, что вы можете определить структуру данных так, как хотите, однако есть некоторые зарезервированные утверждения, такие как те, что использовались выше:

  • iat — метка времени выдачи токена.
  • jti — уникальная строка, которая может использоваться для проверки токена, но это противоречит отсутствию централизованных полномочий эмитента.
  • iss — строка, содержащая имя или идентификатор приложения эмитента. Может быть доменным именем и может использоваться для удаления токенов из других приложений.
  • nbfnbf времени, когда токен должен начать считаться действительным. Должно быть равно или больше чем iat . В этом случае токен начнет действовать 10 секунд
    после выдачи.
  • exp — отметка времени, когда токен должен перестать быть действительным. Должно быть больше, чем iat и nbf . В этом случае токен истекает через 60 секунд после выдачи.

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

Преобразовать этот массив в JWT очень просто:

 <?php /* * Code here... */ /* * Extract the key, which is coming from the config file. * * Best suggestion is the key to be a binary string and * store it in encoded in a config file. * * Can be generated with base64_encode(openssl_random_pseudo_bytes(64)); * * keep it secure! You'll need the exact key to verify the * token later. */ $secretKey = base64_decode($config->get('jwtKey')); /* * Encode the array to a JWT string. * Second parameter is the key to encode the token. * * The output string can be validated at http://jwt.io/ */ $jwt = JWT::encode( $data, //Data to be encoded in the JWT $secretKey, // The signing key 'HS512' // Algorithm used to sign the token, see https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3 ); $unencodedArray = ['jwt' => $jwt]; echo json_encode($unencodedArray); 

JWT::encode() позаботится обо всем (преобразование массива в JSON, создание заголовков, подписание полезной нагрузки и кодирование окончательной строки). Вы захотите сделать свой секретный ключ длинной двоичной строкой, закодировать его в файле конфигурации и никогда не раскрывать его. Иметь его прямо в коде — плохая идея.

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

 $(function(){ var store = store || {}; /* * Sets the jwt to the store object */ store.setJWT = function(data){ this.JWT = data; } /* * Submit the login form via ajax */ $("#frmLogin").submit(function(e){ e.preventDefault(); $.post('auth/token', $("#frmLogin").serialize(), function(data){ store.setJWT(data.JWT); }).fail(function(){ alert('error'); }); }); }); 

Теперь давайте восстановим ресурс, который защищен нашим механизмом JWT.

При нажатии на кнопку «Получить ресурс >>», если все в порядке, вы должны увидеть изображение в серой области. Давайте используем ajax-вызов для отправки запроса в службу ресурсов:

 $("#btnGetResource").click(function(e){ e.preventDefault(); $.ajax({ url: 'resource/image', beforeSend: function(request){ request.setRequestHeader('Authorization', 'Bearer ' + store.JWT); }, type: 'GET', success: function(data) { // Decode and show the returned data nicely. }, error: function() { alert('error'); } }); }); 

Пожалуйста, обратите внимание на параметр beforeSend . Мы сообщаем jQuery, что перед каждым запросом через этот вызов нам нужно установить заголовок Authorization с содержимым JWT в формате Bearer [JWT] . Поэтому, когда мы нажимаем кнопку, делается следующий запрос:

 GET /resource.php HTTP/1.1 Host: yourhost.com Connection: keep-alive Accept: */* X-Requested-With: XMLHttpRequest Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0 

Теперь мы можем увидеть, что такое защищенный ресурс:

Вот как мы проверяем токен в службе ресурсов.

 <?php chdir(dirname(__DIR__)); require_once('vendor/autoload.php'); use Zend\Config\Config; use Zend\Config\Factory; use Zend\Http\PhpEnvironment\Request; /* * Get all headers from the HTTP request */ $request = new Request(); if ($request->isGet()) { $authHeader = $request->getHeader('authorization'); /* * Look for the 'authorization' header */ if ($authHeader) { /* * Extract the jwt from the Bearer */ list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s'); if ($jwt) { try { $config = Factory::fromFile('config/config.php', true); /* * decode the jwt using the key from config */ $secretKey = base64_decode($config->get('jwtKey')); $token = JWT::decode($jwt, $secretKey, array('HS512')); $asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/')); /* * return protected asset */ header('Content-type: application/json'); echo json_encode([ 'img' => $asset ]); } catch (Exception $e) { /* * the token was not able to be decoded. * this is likely because the signature was not able to be verified (tampered token) */ header('HTTP/1.0 401 Unauthorized'); } } else { /* * No token was able to be extracted from the authorization header */ header('HTTP/1.0 400 Bad Request'); } } else { /* * The request lacks the authorization token */ header('HTTP/1.0 400 Bad Request'); echo 'Token not found in request'; } } else { header('HTTP/1.0 405 Method Not Allowed'); } 

Я использую Zend\Http\PhpEnvironment\Request чтобы немного упростить работу с извлечением типов HTTP-запросов и заголовков:

 $request = new Request(); if ($request->isGet()) { //Will only process HTTP GET requests. $authHeader = $request->getHeader('authorization'); // ... 

Теперь давайте выясним, есть ли в заголовке авторизации строка JWT:

 /* * Look for the 'authorization' header */ if ($authHeader) { /* * Extract the JWT from the Bearer */ list($jwt) = sscanf( $authHeader->toString(), 'Authorization: Bearer %s'); // MORE CODE } 

Таким образом, переменная $jwt будет содержать содержимое потенциального JWT.

Если вы не хотите иметь дело с заголовками HTTP-авторизации, вы можете выбрать одну из альтернатив: включить токен в запрос в качестве параметра URL:

 GET /resource.php?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0 HTTP/1.1 Host: yourhost.com Connection: keep-alive Accept: */* X-Requested-With: XMLHttpRequest 

Давайте попробуем декодировать JWT сейчас. Помните секретный ключ, который мы использовали ранее для создания токена? Это жизненно важная часть процесса декодирования здесь:

 $secretKey = base64_decode($config->get('jwtKey')); /* * decode the JWT using the key from config */ $token = JWT::decode($jwt, $secretKey, array('HS512')); 

Если процесс декодирования JWT завершится неудачно, это может быть так:

  1. Количество предоставленных сегментов не соответствует стандарту 3, как описано ранее.
  2. Заголовок или полезная нагрузка не являются допустимой строкой JSON
  3. Подпись недействительна, что означает, что данные были подделаны!
  4. Утверждение nbf устанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
  5. iat устанавливается в JWT с отметкой времени, когда текущая отметка времени меньше этой.
  6. Утверждение exp устанавливается в JWT с отметкой времени, когда текущая отметка времени больше этой.

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

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

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

Наконец, мы запрашиваем изображение с lorempixel.com , base64 кодируем его и возвращаем в виде строки ответа json:

 $asset = base64_encode(file_get_contents('http://lorempixel.com/200/300/cats/')); /* * return protected asset */ header('Content-type: application/json'); echo json_encode([ 'img' => $asset ]); 

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

Вывод

С этого момента вы можете попытаться реализовать JWT в своем следующем API, возможно, попробовав некоторые другие алгоритмы подписи, которые используют асимметричные ключи, такие как RS256 или интегрировать его в существующий сервер аутентификации OAUTH2 в качестве ключа API. Все ваши конструктивные отзывы приветствуются, а также любые вопросы или комментарии.