Делаем аутентификацию в REST API – Уголок системного программиста

Add a gateway class for the person table

There are many patterns for working with databases in an object-oriented context, ranging from simple execution of direct SQL statements when needed (in a procedural way) to complex ORM systems (two of the most popular ORM choices in PHP are Eloquent and Doctrine).

For our simple API, it makes sense to use a simple pattern as well so we’ll go with a Table Gateway. We’ll even skip creating a Person class (as the classical pattern would require) and just go with the PersonGateway class.

src/TableGateways/PersonGateway.php

Obviously, in a production system, you would want to handle the exceptions more gracefully instead of just exiting with an error message.

Here are some examples of using the gateway:

$personGateway = new PersonGateway($dbConnection);


// return all records
$result = $personGateway->findAll();

// return the record with id = 1
$result = $personGateway->find(1);

// insert a new record
$result = $personGateway->insert([
    'firstname' => 'Doug',
    'lastname' => 'Ellis'
]);

// update the record with id = 10
$result = $personGateway->update(10, [
    'firstname' => 'Doug',
    'lastname' => 'Ellis',
    'secondparent_id' => 1
]);

// delete the record with id = 10
$result = $personGateway->delete(10);

We will implement a REST API now with the following endpoints:

// return all records
GET /person

// return a specific record
GET /person/{id}

// create a new record
POST /person

// update an existing record
PUT /person/{id}

// delete an existing record
DELETE /person/{id}

We’ll create a /public/index.php file to serve as our front controller and process the requests, and a src/Controller/PersonController.php to handle the API endpoints (called from the front controller after validating the URI).

public/index.php

Assumptions

Following are the assumptions made while developing this api.

No framework was used while building this API

No separate framework was used for routing

The bakcend is written purely in PHP

The database is MySQL, so it is assumed to be on the system

The unique ids of the items are assumed to be numeric (AUTO-INCREMENT)

Create application layer components

In this section, we’ll create the remaining files that are required for our demo application to work.

Create model classes

In this section, we’ll create the necessary model classes.

Create the Model/Database.php file with the following contents.

Create the php project skeleton for your rest api

We’ll start by creating a /src directory and a simple composer.json file in the top directory with just one dependency (for now): the DotEnv library which will allow us to keep our Okta authentication details in a .env file outside our code repository:

composer.json

{
    "require": {
        "vlucas/phpdotenv": "^2.4"
    },
    "autoload": {
        "psr-4": {
            "Src\": "src/"
        }
    }
}

We’ve also configured a PSR-4 autoloader which will automatically look for PHP classes in the /src directory.

We can install our dependencies now:

composer install

We now have a /vendor directory, and the DotEnv dependency is installed (we can also use our autoloader to load our classes from /src with no include() calls).

Let’s create a .gitignore file for our project with two lines in it, so the /vendor directory and our local .envfile will be ignored:

vendor/
.env

Next we’ll create a .env.example file for our Okta authentication variables:

.env.example

OKTAAUDIENCE=api://default
OKTAISSUER=
SCOPE=
OKTACLIENTID=
OKTASECRET=

and a .env file where we’ll fill in our actual details from our Okta account later (it will be ignored by Git so it won’t end up in our repository).

We’ll need a bootstrap.php file which loads our environment variables (later it will also do some additional bootstrapping for our project).

bootstrap.php

Implementing simple authentication for php rest api

One of the major points of REST as a concept is to avoid the use of session state so that it’s easier to scale the resources of your REST endpoint horizontally. If you plan on using PHP’s $_SESSION as outlined in your question you’re going to find yourself in a difficult position of having to implement shared session storage in the case you want to scale out.

While OAuth would be the preferred method for what you want to do, a full implementation can be more work than you’d like to put in. However, you can carve out something of a half-measure, and still remain session-less. You’ve probably even seen similar solutions before.

  1. When an API account is provisioned generate 2 random values: a Token and a Secret.
  2. When a client makes a request they provide:
    • The Token, in plaintext.
    • A value computed from a unique, but known value, and the Secret. eg: an HMAC or a cryptographic signature
  3. The REST endpoint can then maintain a simple, centralized key-value store of Tokens and Secrets, and validate requests by computing the value.

In this way you maintain the “sessionless” REST ideal, and also you never actually transmit the Secret during any part of the exchange.

Client Example:

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));
$stamp  = "2022-10-12T23:54:50 00:00";        // date("c");
$sig    = hash_hmac('SHA256', $stamp, base64_decode($secret));
// Result: "1f3ff7b1165b36a18dd9d4c32a733b15c22f63f34283df7bd7de65a690cc6f21"

$request->addHeader("X-Auth-Token: $token");
$request->addHeader("X-Auth-Signature: $sig");
$request->addHeader("X-Auth-Timestamp: $stamp");

Server Example:

$token  = $request->getToken();
$secret = $auth->getSecret($token);
$sig    = $request->getSignature();

$success = $auth->validateSignature($sig, $secret);

It’s worth noting that if decide to use a timestamp as a nonce you should only accept timestamps generated within the last few minutes to prevent against replay attacks. Most other authentication schemes will include additional components in the signed data such as the resource path, subsets of header data, etc to further lock down the signature to only apply to a single request.

When this answer was originally written in 2022 JWTs were quite new, [and I hadn’t heard of them] but as of 2020 they’ve solidly established their usefulness. Below is an example of a manual implementation to illustrate their simplicity, but there are squillions of libs out there that will do the encoding/decoding/validation for you, probably already baked into your framework of choice.

function base64url_encode($data) {
  $b64 = base64_encode($data);
  if ($b64 === false) {
    return false;
  }
  $url = strtr($b64, ' /', '-_');
  return rtrim($url, '=');
}

$token  = "Bmn0c8rQDJoGTibk";                 // base64_encode(random_bytes(12));
$secret = "yXWczx0LwgKInpMFfgh0gCYCA8EKbOnw"; // base64_encode(random_bytes(24));

// RFC-defined structure
$header = [
    "alg" => "HS256",
    "typ" => "JWT"
];

// whatever you want
$payload = [
    "token" => $token,
    "stamp" => "2020-01-02T22:00:00 00:00"    // date("c")
];

$jwt = sprintf(
    "%s.%s",
    base64url_encode(json_encode($header)),
    base64url_encode(json_encode($payload))
);

$jwt = sprintf(
    "%s.%s",
    $jwt,
    base64url_encode(hash_hmac('SHA256', $jwt, base64_decode($secret), true))
);

var_dump($jwt);

Yields:

string(167) "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IkJtbjBjOHJRREpvR1RpYmsiLCJzdGFtcCI6IjIwMjAtMDEtMDJUMjI6MDA6MDArMDA6MDAifQ.8kvuFR5xgvaTlOAzsshymHsJ9eRBVe-RE5qk1an_M_w"

and can be validated by anyone that adheres to the standard, which is pretty popular atm.

Anyhow, most APIs tack them into the headers as:

$request->addHeader("Authorization: Bearer $jwt");

Learn more about php, secure rest apis, and oauth 2.0 client credentials flow

You can find the whole code example here: GitHub link.

If you would like to dig deeper into the topics covered in this article, the following resources are a great starting point:

Build a Simple REST API in PHP was originally published on the Okta developer blog on March 8, 2022. 

References

Since this is my first php project, I referred to a lot of places. I am mentioning a few below.

Setting up the skeleton

In this section, we’ll briefly go through the project structure.

Let’s have a look at the following structure.

Steps for setting it up

Server
I used apache while development. But this should work on any server with supports .htaccess file configuration, since I use it for routing.

Database
The code assumes MySQL as server.

I have written scripts to automate the database setup.

Change the credentials in db_operations/connect.php file. Once that is done, execute db_operations/setup_database.php file.

The basecontroller.php file

Create the Controller/Api/BaseController.php file with the following contents. The BaseController class contains the utility methods that are used by other controllers.

The controller directory

In this section, we’ll implement controllers that hold the majority of our application logic.

The inc directory

For starters, we’ll create the necessary configuration files.

Create the inc/config.php file with the following contents.

The index.php file

The index.php file is the entry-point of our application. Let’s see how it looks.

Вход в систему

Метод идентифицирует пользователя по переданным email и паролю и если все в порядке, генерирует токен доступа, устанавливает дату действия токена и возвращает его в json.

Выход из системы

В этом методе токен просто отзывается у авторизованного пользователя.

Маршруты для аутентификации

После реализации методов контроллера аутентификации создадим соответствующие маршруты в файле /routes/api.php:

Route::group(['namespace' => 'Api'], function () {
	Route::post('register', 'AuthController@register');
	Route::post('login', 'AuthController@login');
	Route::post('logout', 'AuthController@logout')->middleware('auth:api');
});

Два первых маршрута – register и login работают для неавторизованных пользователей. Третий маршрут logout работает через middleware auth – только для авторизованных пользователей. Для запросов по этому маршруту следует передавать в запросе заголовок с токеном доступа:

Authorization: Bearer erJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9…

Теперь все готово. Можно регистрировать нового пользователя через вызов /api/register, передав параметры name, email и password, авторизоваться через вызов /api/login, передав email и password, и выходить из системы через вызов /api/logout.

Объясните авторизацию restful

@IOleg, теперь вопрос куда яснее. Я так понимаю, вы создаете API, в этом случае сессии нельзя использовать – привязка клиент-сессия осуществляется через куки, ее можно имитировать через курл, но никто так не делает, слишком мног омороки с инвалидацией. Обычно создается секретный ключ, который знают только сервис и клиент; после этого с помощью секретного ключа создается подпись к каждому запросу, которая позволяет установить, что запрос

  1. Действительно был послан тем клиентом, который указан в запросе
  2. Все параметры запросы действительно прописаны оригинальным клиентом (запрос может быть перехвачен по пути, после чего в него можно напихать “своих” параметров, если этой проверки не будет)
  3. Запрос действительно отправлен клиентом только что, а не какой-то плохой MITM взял и заново послал запрос, который уже проходил месяц назад.

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

  1. Сервер и клиент обмениваются секретным значением. Делается это единожды и не по тому каналу связи, по которому потом будут идти запросы, обычно он выдается в неавтоматическом режиме в админке сервиса разработчику клиента.
  2. Клиент формирует полный запрос, причем не конкретные объекты, представляющие HTTP-запрос, а именно набор параметров – URL, список аргументов, время запроса, необходимые ключи (В данном случае – один секретный), свой айдишник у сервера, nonce или время отправки запроса (в этом случае время запроса добавляется обычным аргументом; ниже – подробнее)
  3. Клиент сериализует все данные и получает из них хэш и включает аргументом в запрос. Хэш зависит от секретного ключа, времени запроса/nonce и всех параметров запроса – в результате злоумышленник должен знать весь этот набор для создания имитирующего запроса, а с учетом использования времени запроса или nonce он не может просто взять и повторить запрос.
  4. Сервер получает запрос, разбивает на аргументы, составляет точно такой же хэш и убеждается в корректности/некорректности запроса. Если запрос корректен, то он выполняется и обратно отправляется ответ.

Таким образом, явной авторизации (авторизовался и забыл) нет – подписывается каждый запрос, а “пароль” не передается никогда.

Что там с датой и nonce: есть уже упомянутый вид атак, называемых replay attack. Допустим, клиент запросил сервер поменять имя какого-нибудь проекта на “аааа”, а затем еще раз – на “ббб”. Подслушивающая Ева в этом случае может повторить запрос смены имени на “аааа” то количество раз, которое ей покажется достаточным, чтобы насолить серверу и клиенту. Против этого есть два способа борьбы:

  1. Клиент включает в запрос (и хэш) время отправки запроса. Сервер проверяет время прихода запроса, и, если время вышло за пределы позволенного временного отрезка (скажем, пяти минут), то сервер инвалидирует запрос и отказывается его выполнять. Подменять в запросе время отправки бессмысленно, т.к. в этом случае не сойдется хэш, и сервер инвалидирует запрос уже на этапе проверки хэша.
  2. Сервер выдает клиентам nonce – number used once. В этом случае сервер выдает и запоминает уникальные одноразовые значения для подписи запрослв (те же хэши, например) своим клиентам, после чего, когда приходит запрос, он проверяет наличие такого nonce в своем репозитарии, и если его нет, инвалидирует запрос, а если есть, то этот nonce удаляется из репозитария, и Ева опять не может ни повторить запрос, ни использовать уже использованный nonce. Сервер при этом может не просто хранить nonce, но и запоминать привязку по айпи и дате (хотя на самом деле подменить айпи пакета не составляет особого труда, только вот ответ в этом случае уйдет не тому, кто подменил).

nonce чаще всего используется в том случае, когда клиент – это браузер пользвателя и там сложно говорить об api в классическом смысле; в частности, csrf-token – это nonce, обычно сгененрированный на основе ip пользователя.

Регистрация пользователя

В этом методе создается объект пользователя по переданным параметрам (имя, email, пароль), сохраняется в БД и возвращается сообщение об у спешной регистрации.

Похожее:  Вход на сайт через Вконтакте

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *