REST API аутентификация с использованием Laravel Passport

REST API аутентификация с использованием Laravel Passport

В данной статье рассмотрим как в Laravel создать API аутентификацию с помощью официального пакета Passport.

Для начала пару слов о том, как всё происходит. Как известно, в период между запросами REST-приложения не хранят информацию о состоянии клиента. Для аутентификации обычно используются токены. Другими словами:

  1. Пользователь отправляет серверу логин и пароль
  2. Если всё ок, сервер генерирует токен и отправляет клиенту
  3. Клиент (обычно в хедере) отправляет данный токен с каждым запросом
  4. На стороне сервера проверяется токен, и опять-таки, если всё ок - отправляется ответ (разумеется, в том случае, если для ендпойнта требуется авторизация)

Пакет laravel/passport обеспечивает полную реализацию сервер OAuth2, и если Вы не знакомы с темой после прочтения статьи настоятельно рекомендую ознакомиться со следующими материалами: JSON Web Tokens, Laravel Passport, OAuth 2.0 Server.

Для этой статьи я использую Laravel 5.7.19 и PostgreSQL 10.5

Установка и конфигурация

Шаг 1

Устанавливаем пакет:

composer require laravel/passport
 
Шаг 2

Выполняем миграции:

php artisan migrate
 
Шаг 3

Создаём ключи шифрования, которые необходимы для генерирования безопасных токенов:

php artisan passport:install

 

Эта команда также создаст клиентов "personal access" и "password grant", которые будут использоваться при генерации токенов. 

Шаг 4

Добавим трейт HasApiToken в модель User:

<?php

namespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasApiTokens, Notifiable;
    
    ...
}
 
Шаг 5

Далее пропишем вызов роутов в методе boot() провайдера AuthServiceProvider (app/Providers/AuthServiceProvider.php):

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
        Passport::routes();
    }
}
 
Шаг 6

Наконец, в файле конфигурации config/auth.php в качестве параметра драйвера для защиты api аутентификации установим значение passport:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

 

Советую очистить кеш конфигурации, тем более, если проект не нов:

php artisan config:clear

 

Роуты и контроллеры

В приципе все методы можно было бы поместить в AuthController, но я предлагаю сделать структуру аналогичную web - то есть создать отдельные контроллеры для логина, регистрации и т.д. и расположить их в каталоге Api/Auth. Также, поскольку каждый из контроллеров будет содержать только один метод, будем использовать Single Action Controllers.

RegisterController и RegisterFormRequest

Валидацию предлагаю вынести из контроллера в отдельный класс. Для этого выполним:

php artisan make:request Api/Auth/RegisterFormRequest

 

Открываем только что созданный файл app/Http/Requests/Api/Auth/RegisterFormRequest.php и вставляем следующий код:

<?php

namespace App\Http\Requests\Api\Auth;

use Illuminate\Foundation\Http\FormRequest;

class RegisterFormRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:6', 'confirmed'],
        ];
    }
}

 

Разбирать код не вижу смысла, поскольку тема была ранее раскрыта здесь. Чтобы создать контроллер с единственным методом следует использовать флаг -i (сокращение от --invokable):

php artisan make:controller -i Api/Auth/RegisterController

 

Вместо обычного Request внедряем созданный ранее RegisterFormRequest, после чего остаётся только создать пользователя и вернуть ответ. Не будем возвращать токен, а лишь укажем, что пользователь успешно зарегистрирован, и теперь может войти в систему, используя свой почтовый адрес и пароль:

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Auth\RegisterFormRequest;
use App\User;

class RegisterController extends Controller
{
    public function __invoke(RegisterFormRequest $request)
    {
        $user = User::create(array_merge(
            $request->only('name', 'email'),
            ['password' => bcrypt($request->password)],
        ));

        return response()->json([
            'message' => 'You were successfully registered. Use your email and password to sign in.'
        ], 200);
    }
}

 

LoginController

Аналогичным образом создаём контроллер для входа:

php artisan make:controller -i Api/Auth/LoginController

 

Поскольку код контроллера требует пояснений, разберём его по частям. Итак, берём из запроса email и пароль и пробуем войти. Если, данные не верны - отправим соответствующее сообщение и код ошибки:

$credentials = $request->only('email', 'password');

if (!Auth::attempt($credentials)) {
    return response()->json([
        'message' => 'You cannot sign with those credentials',
        'errors' => 'Unauthorised'
    ], 401);
}

 

Если же всё прошло "как надо", сгенерируем токен:

$token = Auth::user()->createToken(config('app.name'));

 

И вот здесь будьте внимательны - в переменной $token находится не непосредственно jwt-токен, а объект, который в моём случае в Postman выглядит так: 

{
    "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6Ijk2YWYzNDBlZjVlNTMxZmY0MTc4NmJkM2NiN2QwZjk4Yzh...",
    "token": {
        "id": "96af340ef5e531ff41786bd3cb7d0f98c8c323b30c1a4857a027aa064b1c40adb50a5a0e6668ae46",
        "user_id": 3,
        "client_id": 1,
        "name": "Test App",
        "scopes": [],
        "revoked": false,
        "created_at": "2019-01-15 19:49:37",
        "updated_at": "2019-01-15 19:49:37",
        "expires_at": "2020-01-15 19:49:37"
    }
}

 

Теперь, когда объект токена перед глазами, станут понятны дальнейшие действия. Будем исходить из предположения, что в форме логина мог быть отмечен чекбокс "Запомнить меня". Если это так, установим срок жизни токена в один месяц, если нет - один день, и затем сохраним изменения:

$token->token->expires_at = $request->remember_me ?
    Carbon::now()->addMonth() :
    Carbon::now()->addDay();

$token->token->save();

 

Полный код контроллера:

<?php

namespace App\Http\Controllers\Api\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;

class LoginController extends Controller
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function __invoke(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (!Auth::attempt($credentials)) {
            return response()->json([
                'message' => 'You cannot sign with those credentials',
                'errors' => 'Unauthorised'
            ], 401);
        }

        $token = Auth::user()->createToken(config('app.name'));
        $token->token->expires_at = $request->remember_me ?
            Carbon::now()->addMonth() :
            Carbon::now()->addDay();

        $token->token->save();

        return response()->json([
            'token_type' => 'Bearer',
            'token' => $token->accessToken,
            'expires_at' => Carbon::parse($token->token->expires_at)->toDateTimeString()
        ], 200);
    }
}
 
LogoutController

Здесь проще некуда - "отзываем" токен:

<?php

namespace App\Http\Controllers\Api\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class LogoutController extends Controller
{
    public function __invoke(Request $request)
    {
        $request->user()->token()->revoke();

        return response()->json([
            'message' => 'You are successfully logged out',
        ]);
    }
}
 
Роуты

Внимание: если Вы определите маршруты перед созданием Single action controllers, в процессе создания контроллеров в консоли будет вылетать ошибка с требованием указать метод в роутах. Поэтому рекомендую придерживаться последовательности, описанной в этой статье (либо пойти стандартным путём и разместить все методы в AuthController). 

Откроем файл routes/api.php и пропишем маршруты:

Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

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

 

Небольшое пояснение: следует учитывать, что помимо аутентификации в каталоге Api будут располагаться и другие контроллеры или группы контроллеров, поэтому логичнее в группу с общим неймспейсом вложить другие подгруппы (например, categories, products и т.д.)

На этом на сегодня всё. Удачи!