Laravel: Беспарольная аутентификация

Источник: «Passwordless Authentication in Laravel»
Иногда мы не хотим, чтобы у пользователей были пароли. Иногда мы хотим отправить волшебную ссылку на адрес электронной почты пользователя, чтобы он кликнул по ней и получил доступ.

В этой статье я расскажу о процессе, который вы можете использовать для самостоятельной реализации данного решения. Основное понимание в этом рабочем процессе уделяется созданию подписанного URL-адреса, который позволит нам отправлять определённый URL-адрес на адрес электронной почты пользователя, и только этот человек должен иметь доступ к этому адресу.

Сначала мы удалим поле пароля из нашей миграции, модели и фабрики модели. Поскольку оно не понадобиться, мы хотим удалить его, так как по умолчанию оно не является столбцом, допускающим значение NULL. Это относительно простой процесс, поэтому я не буду показывать примеры кода для этой части. Пока мы это делаем, мы можем удалить таблицу сброса пароля, так как у нас не будет сброса пароля.

Следующей мы рассмотрим маршрутизацию. Мы можем создать маршрут входа в систему как простой маршрут представления, так как в этом примере будем использовать Larawire. Давайте посмотрим на регистрацию этого маршрута:

Route::middleware(['guest'])->group(static function (): void {
Route::view('login', 'app.auth.login')->name('login');
});

Мы обернули этот маршрут в guest middleware для вызова перенаправления, если пользователь уже вошёл в систему. Я не буду рассматривать пользовательский интерфейс для примера, но в конце руководства есть ссылка на репозиторий GitHub. Давайте рассмотрим компонент Livewire, который мы будем использовать для формы входа.

final class LoginForm extends Component
{
public string $email = '';

public string $status = '';

public function submit(SendLoginLink $action): void
{
$this->validate();

$action->handle(
email: $this->email,
);

$this->status = 'An email has been sent for you to log in.';
}

public function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::exists(
table: 'users',
column: 'email',
),
]
];
}

public function render(): View
{
return view('livewire.auth.login-form');
}
}

У нашего компонента два свойства — $email для захвата поля ввода формы, и $status с которым нам не нужно полагаться на сессию запроса. У нас есть метод возвращающий правила валидации. Это мой предпочтительный подход к правилам валидации компонента Livewire. Метод submit() основной для этого компонента, и это соглашение об именах, которое я использую с компонентами формы. Для меня это имеет большой смысл, но не стесняйтесь использовать свой метод именования. Мы используем контейнер Laravel для внедрения action класса в этот метод, чтобы поделиться логикой создания и отправки подписанного URL-адреса. Всё, что нам нужно сделать, это передать введённый адрес электронной почты в action и установить статус, предупреждающий, что электронное письмо отправляется.

Теперь давайте пройдёмся по action, который мы хотим использовать.

final class SendLoginLink
{
public function handle(string $email): void
{
Mail::to(
users: $email,
)->send(
mailable: new LoginLink(
url: URL::temporarySignedRoute(
name: 'login:store',
parameters: [
'email' => $email,
],
expiration: 3600,
),
)
);
}
}

Этот action нужен только для отправки электронного письма. Мы можем настроить его на постановку в очередь, если хотим, но при работе с action, требующим быстрой обработки, лучше поставить его в очередь, если мы создаём API. У нас есть почтовый класс LoginLink, передаваемый через URL-адрес, который хотим использовать. Наш URL создаётся путём передачи имени маршрута, для которого мы хотим сгенерировать маршрут. И передачи параметров, которые мы хотим использовать как часть подписи.

final class LoginLink extends Mailable
{
use Queueable, SerializesModels;

public function __construct(
public readonly string $url,
) {}

public function envelope(): Envelope
{
return new Envelope(
subject: 'Your Magic Link is here!',
);
}

public function content(): Content
{
return new Content(
markdown: 'emails.auth.login-link',
with: [
'url' => $this->url,
],
);
}

public function attachments(): array
{
return [];
}
}

Наш mailable класс относительно прост и не сильно отличается от стандартного. Мы передаём строку для URL. Затем мы хотим передать её в markdown представлении в контент.

<x-mail::message>
# Login Link

Use the link below to log into the {{ config('app.name') }} application.

<x-mail::button :url="$url">
Login
</x-mail::button>

Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

Пользователь получит это письмо и кликнет по ссылке, которая приведёт его к подписанному URL-адресу. Давайте зарегистрируем этот маршрут и посмотрим, как он выглядит.

Route::middleware(['guest'])->group(static function (): void {
Route::view('login', 'app.auth.login')->name('login');
Route::get(
'login/{email}',
LoginController::class,
)->middleware('signed')->name('login:store');
});

Мы хотим использовать контроллер для этого маршрута и обязательно добавляем signed middleware. Теперь давайте посмотрим на контроллер, что бы увидеть, как мы обрабатываем подписанные URL-адреса.

final class LoginController
{
public function __invoke(Request $request, string $email): RedirectResponse
{
if (! $request->hasValidSignature()) {
abort(Response::HTTP_UNAUTHORIZED);
}

/**
* @var User $user
*/

$user = User::query()->where('email', $email)->firstOrFail();

Auth::login($user);

return new RedirectResponse(
url: route('dashboard:show'),
);
}
}

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

Теперь наш пользователь успешно вошёл в систему, и наше путешествие завершено. Тем не менее нам нужен также посмотреть на маршрут регистрации. Давайте добавим этот маршрут следующим. Опять, это будет маршрут представления.

Route::middleware(['guest'])->group(static function (): void {
Route::view('login', 'app.auth.login')->name('login');
Route::get(
'login/{email}',
LoginController::class,
)->middleware('signed')->name('login:store');

Route::view('register', 'app.auth.register')->name('register');
});

Снова мы используем компонент Livewire для регистрационной формы — точно так же, как мы делали это с процессом входа в систему.

final class RegisterForm extends Component
{
public string $name = '';

public string $email = '';

public string $status = '';

public function submit(CreateNewUser $user, SendLoginLink $action): void
{
$this->validate();

$user = $user->handle(
name: $this->name,
email: $this->email,
);

if (! $user) {
throw ValidationException::withMessages(
messages: [
'email' => 'Something went wrong, please try again later.',
],
);
}

$action->handle(
email: $this->email,
);

$this->status = 'An email has been sent for you to log in.';
}

public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:2',
'max:55',
],
'email' => [
'required',
'email',
]
];
}

public function render(): View
{
return view('livewire.auth.register-form');
}
}

Мы берём имя пользователя, адрес электронной почты, и свойство $status вместо повторного использования запроса сессии. Мы снова используем метод rules, чтобы вернуть правила валидации для этого запроса. Мы возвращаемся к методу submit, где на этот раз хотим внедрить два action.

CreateNewUser — action используемый для создания и возврата нового пользователя на основе предоставленной информации. Если по какой-то причине это не удаётся, мы выдаём исключение валидации в электронном письме. Затем используем action SendLoginLink, которое использовали в форме логина, для сведения к минимуму дублирования кода.

final class CreateNewUser
{
public function handle(string $name, string $email): Builder|Model
{
return User::query()->create([
'name' => $name,
'email' => $email,
]);
}
}

Мы могли бы переименовать маршрут хранения логина, но технически это то, что мы снова делаем. Создаём пользователя. Затем мы хотим авторизовать пользователя.

Это один из многих подходов, используемых для реализации аутентификации без пароля, но это один из подходов, который действительно работает. Вот ссылка на репозиторий GitHub, и если вы считаете, что это можно улучшить, не стесняйтесь оставлять PR!

Дополнительные материалы

Предыдущая Статья

Python: Виртуальные среды — это просто

Следующая Статья

Laravel: Как написать хелпер