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

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

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

# composer require paragonie/random_compat:2.*
# composer require laravel/passport=~4.0

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

# php artisan migrate

должно быть примерно так:

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

# php artisan passport:install

должно быть примерно так:

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

Добавить трейт HasApiToken в модель User:
файл: app/User.php

<?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;
    
    ...
}

Прописать вызов роутов в методе 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();
    }
}

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

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

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

очистить кеш конфигурации

# php artisan config:clear

 

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

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