Laravelでパスワードレス認証(マジックリンク)を実装する + Laravel Voltでログアウトする
これからSNSを作ろう、作ったSNSを世にだそうとしている開発者につぐ。
— 👹秋田の猫🐱 (@ritou) 2023年7月3日
とりあえずでパスワード認証を使うな!
Twitter, Gmail, Appleログインとかの方が良いかもしれないけど今回はマジックリンクで実装する。
下記のライブラリで実装する。
こっちでも良いかもしれない。
環境構築
Laravel Valetで環境構築をする。
今までLaravel Sail使ってたけどmacOSではLaravel Valetの方が便利みたい。
実際使ってみたけどいちいちsail upとか打たなくてすむし、複数のサイトを気楽に管理できるから今のところ良さそう。
ファイル変更したときにブラウザリロードする必要があるけど、それも少し調べれば解決方法あるかも。
laravel new resend
valet open resend # 多分 http://resend.testというサイトが開く
resend.com
マジックリンクをメールで送信する必要があるので今回はResendを使用する。
以前アカウントを作って何も使ってなかったから使おうと思っただけで、他のサービスでも問題ないと思います。
Userモデルの変更(primary keyをULIDにする)
最終的には下記のようなリンクが生成されるが、この時ユーザーIDが含まれて個人的に少し嫌なので、IDをULIDに変更する。
# idが連番 http://resend.test/magic-login/1?expires=1690636947&user_type=app-models-user&signature=07a7b9d4bbc20d7c889edfe627b9e7a7d24236c681f26172e6d13bac185a348e # idがulid http://resend.test/magic-login/01h6ce5tydqwtmvpe2j17ahhp1?expires=1690636947&user_type=app-models-user&signature=07a7b9d4bbc20d7c889edfe627b9e7a7d24236c681f26172e6d13bac185a348e
2014_10_12_000000_create_users_table.php
<?php return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('users', function (Blueprint $table) { // $table->id(); $table->ulid('id')->primary(); // <--- idをULIDにする $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('users'); } };
User.php
<?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable, HasUlids; // <- HasUlids を追加
Webで検索するとboot()とかでidにulidセットしたり、$keyType = 'string';にしたり色々方法見かけたけど今はHasUlidsだけで大丈夫そう(多分)
users
のpassword
もいらないかも。
開発ではsqlite
を使うと便利だと思う。
sqlite
使う時は.env
を少し修正する必要がある。
.env
DB_CONNECTION=sqlite # <--- 多分デフォルトはmysqlになっているのでsqliteに変更 DB_HOST=127.0.0.1 DB_PORT=3306 # DB_DATABASE=db.sqlite # DB_DATABASEはコメントアウトする DB_USERNAME=root DB_PASSWORD=
sqlite
を使う時はDB_DATABASE
をコメントアウトしないと下記のようなエラーが出る。
理由はなんとなく分かるけどちゃんとは理解してない。
Database file at path [db.sqlite] does not exist. Ensure this is an absolute path to the database.
変更したらmigrate
とdb:seed
をやっておく。
php artisan migrate php artisan db:seed
ログインURL生成
フォーム作って、コントローラーでリクエスト受けて...とかやってると面倒なので今回はcommandを使う。
php artisan make:command Tests/TestGenLoginLink
とりあえず一番最初のユーザーのログインリンクを生成する。
TestGenLoginLink.php
<?php namespace App\Console\Commands\Tests; use App\Models\User; use Grosv\LaravelPasswordlessLogin\LoginUrl; use Illuminate\Console\Command; class TestGenLoginLink extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'test:gen:loginlink'; /** * The console command description. * * @var string */ protected $description = 'ログインリンク生成'; /** * Execute the console command. */ public function handle() { $user = User::first(); $generator = new LoginUrl($user); $url = $generator->generate(); $this->info($url); } }
下記のコマンドで最初のユーザーのログインリンクを生成できる。
$ php artisan test:gen:loginlink http://resend.test/magic-login/01h6h6fk862hrz0t389gymex76?expires=1690650694&user_type=app-models-user&signature=60c851c17e1d2b5060f96c2366a8694731ecf35c24dcff512b97688e69be172a
実際にログインしてみる
welcome.blade.php
を下記のように書き換えてブラウザでURLにアクセスする
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet"/> </head> <body> <div> @if(Auth::check()) logged in: {{Auth::user()->id }} @else guest @endif </div> </body> </html>
ログイン前
ログイン後
ログインリンクをメールで送る
Resend.comではResendファサードが用意されているようだけど、今回はMailファサードを使用する。
メールの内容はMarkdown
を使って記述もできるので今回はそちらを使用する。
php artisan make:mail LoginLink --markdown=emails.auth.login_link
LoginLink.php
とlogin_link.blade.php
が生成されるのでそれぞれ修正する。
Mailableの方に変数を定義しておくと、bladeファイルの方からアクセスできる。
LoginLink.php
<?php namespace App\Mail; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class LoginLink extends Mailable { use Queueable, SerializesModels; /** * Create a new message instance. */ public function __construct( public string $url // <--- ログインリンク用の変数追加 ){} ...
login_link.blade.php
<x-mail::message> <x-mail::button :url="$url"> Login </x-mail::button> Thanks,<br> {{ config('app.name') }} </x-mail::message>
とりあえずこれでメールを送る準備が出来た。
Controller用意してとかやってると面倒なので、ログインリンク送信もCommandを使用する。
今回はUser::first()のログインリンクを指定のメールアドレスに送信しているが、本来はUserに登録されたメールアドレスを使用する(重要)
ライブラリの作者も間違ったユーザーへ送信しないようにREADMEで注意をしている。
The biggest mistake I could see someone making with this package is creating a login link for one user and sending it to another. Please be careful and test your code. I don't want anyone getting mad at me for someone else's silliness.
(このパッケージで誰かが犯しそうな最大のミスは、あるユーザー用のログインリンクを作成し、それを別のユーザーに送信することです。気をつけてコードをテストしてください。他人の愚かな行為で私に怒られたくないのです。)
php artisan make:command Tests/TestSendLoginLink
resend.comは初期設定だと登録したメールアドレスにしか送信できないようなので、もし送信したいメアドを変更したい場合は設定が必要(未確認)
TestSendLoginLink
<?php namespace App\Console\Commands\Tests; use App\Mail\LoginLink; use Grosv\LaravelPasswordlessLogin\LoginUrl; use Illuminate\Console\Command; use Illuminate\Support\Facades\Mail; class TestSendLoginLink extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'test:mail:loginlink'; /** * The console command description. * * @var string */ protected $description = 'ログインリンク送信テスト'; /** * Execute the console command. */ public function handle() { $to = "<resend.comに登録したメアド>"; // 念のため $confirm = $this->confirm("send email to?: $to"); if (!$confirm) { return; } // こんなことは普通やらないけど今回は例としてfirstのユーザーを使用している $user = User::first(); $generator = new LoginUrl($user); $url = $generator->generate(); $result = Mail::to($to)->send(new LoginLink($url)); } }
下記のコマンドで実際にメールを送信できる。
$ php artisan test:mail:loginlink send email to?: xxxx@test.com (yes/no) [no]: > yes $
下記のようなメールが送信されてきてボタンをタップするとログインが完了する。
Laravel Voltでログアウト(おまけ)
先日LaraconUSでLaravel Voltが発表されましたね。
見た感じVueというかReactというかそういったライブラリっぽい書き方をPHPで実現できるライブラリのようです。
Livewireも使ったこと無いけど良い機会なので試しに使ってみようと思います。
ついでにBlade Componentも使ったこと無いのでそれも使ってみる(素のBladeしか使ったことなかった)
Laravel Folioというルーティングライブラリも発表されたが今回は使わない。
Livewire 3.0(beta)とVolt 1.0(beta)をインストールする。
$ composer require livewire/livewire:^3.0@beta livewire/volt:^1.0@beta $ php artisan volt:install
とりあえずrootとなるBlade Componentを作成する。
--view
フラグを付けるとapp/View/Components/
にComponentクラスが生成されない(匿名コンポーネントが生成される。)
$ php artisan make:component app --view
コンポーネントはresources/views/components/
に追加される。
とりあえずwelcome.blade.php
の<body>
内以外をコピーしてくる。
app.blade.php
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Laravel</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet"/> </head> <body> {{$slot}} </body> </html>
Blade Component
に別の要素を追加する時は$slot
を使うので<body>
内は$slot
としている。
そしてwelcome.blade.php
は下記のように書き換えることができる。
welcome.blade.php
<x-app> <div> @if(Auth::check()) logged in: {{Auth::user()->id }} @else guest @endif </div> </x-app>
Volt Componentを追加する。
make:volt
でVolt Componentが生成できる。
生成されたコンポーネントはresources/views/livewire
ディレクトリに生成される。
$ php artisan make:volt auth
下記のようなテンプレートが生成される。
auth.blade.php
<?php use function Livewire\Volt\{state}; // ?> <div> // </div>
ここにログアウト機能を追加していく。
Laravel Voltでログアウトする
最終的にはこんな感じになった
auth.blade.php
<?php use Illuminate\Support\Facades\Auth; use function Livewire\Volt\{computed}; $user = computed(function () { return Auth::user(); }); $logout = fn() => Auth::logout(); ?> <div> @if($this->user) logged in: {{ $this->user->id }}<br/> <button wire:click="logout">Logout</button> @else guest @endif </div>
welcome.blade.php
を下記のように変更する。
<x-app> <livewire:auth /> </x-app>
実行結果。
ログイン状態:
ログアウトボタン押下:
gif動画をアップロードしようと思ったけどなぜか失敗するので静止画。
まとめ
マジックリンク実装ついでに使ったことない(+あまり使ったこと無い)機能も色々使ってみました。
Laravel Volt便利ですね。