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 і т.д.)

На цьому на сьогодні все. Успіхів!