[Laravel Breeze]署名つきURLでログインする
はじめに
Laravel Breezeに署名つきURLを使ってパスワードなしのログイン機能を実装していきます。
署名付きURLは安全なリンクを生成し、一時的なアクセス許可を提供するために使われます。
環境
PHP 8.x
Laravel 10.x
Laravel breeze
sqliste
tl;dr
1. Laravel Breezeを使ってユーザー承認を作成する
2. パスワードを要求する処理を削除する
3. 署名つきURLを生成する
4. 認証メソッドを作成する
5. ルートを追加する
6. メール送信を設定する
laravelプロジェクトを作成する
laravel-passwordless
というプロジェクトを作成します。
breezeも一緒にインストールします。
laravel new laravel-passwordless --breeze
➜ laravel-passwordless git:(main) php artisan breeze:install
┌ Which Breeze stack would you like to install? ───────────────┐
│ Blade with Alpine │
└──────────────────────────────────────────────────────────────┘
┌ Would you like dark mode support? ───────────────────────────┐
│ No │
└──────────────────────────────────────────────────────────────┘
┌ Which testing framework do you prefer? ──────────────────────┐
│ PHPUnit │
└──────────────────────────────────────────────────────────────┘
INFO Installing and building Node dependencies.
added 112 packages, and audited 113 packages in 12s
22 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
> build
> vite build
vite v4.5.0 building for production...
✓ 48 modules transformed.
public/build/manifest.json 0.26 kB │ gzip: 0.13 kB
public/build/assets/app-9c5f9671.css 30.82 kB │ gzip: 6.00 kB
public/build/assets/app-a35cead1.js 72.23 kB │ gzip: 26.83 kB
✓ built in 1.78s
INFO Breeze scaffolding installed successfully.
ローカルでのテスト実装のためDBをsqlite
にします。
DB_CONNECTION=sqlite
DBのマイグレーションを実行します。
➜ laravel-passwordless php artisan migrate
WARN The SQLite database does not exist: database/database.sqlite.
┌ Would you like to create it? ────────────────────────────────┐
│ Yes │
└──────────────────────────────────────────────────────────────┘
INFO Preparing database.
Creating migration table ......................................................... 6ms DONE
INFO Running migrations.
2014_10_12_000000_create_users_table ............................................. 4ms DONE
2014_10_12_100000_create_password_reset_tokens_table ............................. 1ms DONE
2019_08_19_000000_create_failed_jobs_table ....................................... 2ms DONE
2019_12_14_000001_create_personal_access_tokens_table ............................ 2ms DONE
パスワードを要求する処理を削除する
ログインビューにあるパスワードの入力フォームを削除します。
<!-- Password -->
- <div class="mt-4">
- <x-input-label for="password" :value="__('Password')" />
- <x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
- <x-input-error :messages="$errors->get('password')" class="mt-2" />
- </div>
- @if (Route::has('password.request'))
- <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
- </a>
- @endif
パスワードなしのログイン画面:
署名つきURLを生成する
署名つきURLの使い方
use Illuminate\Support\Facades\URL;
$url = URL::temporarySignedRoute(
'route.name', // 使いたいルートの名前
now()->addMinutes(30) // 有効期限
);
// もしくは、URL::temporarySignedRouteメソッドの第3引数にパラメーターを追加できます
$urlWithParameters = URL::temporarySignedRoute(
'route.name',
now()->addMinutes(30),
['parameter' => 'value']
);
authenitcate
メソッドを使わないでメールアドレスで認証するロジックを追加します。
<?php
namespace App\Http\Controllers\Auth;
use App\Models\User;
use Illuminate\View\View;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\RedirectResponse;
use App\Providers\RouteServiceProvider;
class AuthenticatedSessionController extends Controller
{
/**
* Handle an incoming authentication request.
*/
public function store()
{
request()->validate(['email' => 'required']);
// メールアドレスで認証するロジックを追加する
$user = User::where((['email' => request('email')]))->first();
if (!$user) {
return back()->withErrors(['email'=> 'メールアドレスを確認できません。']);
}
// 有効期限が30分の署名つきURLを生成する
$link = URL::temporarySignedRoute('login.token', now()->addMinutes(30), ['user' => $user->id]);
return back()->with(['status' => '入力したメールアドレスにログインリンクを送り致しました。30分以内にログインしてください。']);
}
}
ルートを追加する
Route::get('login/{user}', [AuthenticatedSessionController::class, 'loginViaToken'])
->name('login.token')
->middleware('signed');
Laravelでは、middleware('signed')
メソッドを使用して、ルートに「署名付き」ミドルウェアを適用します。signedミドルウェアは主に、署名されたパラメータを持つURLの完全性を検証するために使用されます。
署名が有効な場合、リクエストはルートのコントローラに進みます。署名が有効でない、または見つからない場合、Laravelは403エラーでリクエストを中止します。
hasValidSignatureでURLを検証することもできる
use Illuminate\Http\Request;
public function yourMethod(Request $request)
{
if ($request->hasValidSignature()) {
// 署名が有効な場合の処理
return view('your-view');
} else {
// 無効な場合の処理
abort(403, 'Unauthorized action.');
}
}
生成された署名つきURL:
http://127.0.0.1:8000/login/1?expires=1699886487&signature=50d515439bc714c0b1b2cb375343e9a126ac1765bd985b84eb22f8164f1112b5
認証メソッドを作成する
ルートで定義したloginViaToken
メソッドを作成します。
public function loginViaToken(User $user)
{
// ユーザーをログインする
Auth::login(($user));
// 新しいセッションIDを生成する
request()->session()->regenerate();
// HOMEにリダイレクトする
return redirect(RouteServiceProvider::HOME);
}
メール送信を設定する
LaravelのNotificationクラスを使ってメール送信機能を作成します。
➜ laravel-passwordless git:(main) ✗ php artisan make:notification login
INFO Notification [app/Notifications/Login.php] created successfully.
メーラーの名前を変えます。
MAIL_MAILER=log
ログイン用リンクを生成します。
// Loginクラスに$linkを渡す
$user->notify(new Login($link));
メールの本文を作成します。
<?php
namespace App\Notifications;
...
class Login extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
// $linkを初期化する
public function __construct(public string $link)
{
//
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
// メール本文の表示を調整する
return (new MailMessage)
->line('クリックしてログインする')
->action('ログイン', url($this->link))
->line('〇〇アプリをご利用いただきありがとうございます!');
}
}
ログイン画面でメールアドレスを入力し、ログインをクリックします。
Laravelのログファイルでメールを送信されたことを確認します。
ログインリンクをクリックしてログインされたことを確認します。
[2023-11-14 12:17:07] local.DEBUG: From: Laravel <hello@example.com>
To: test@example.com
Subject: Login
MIME-Version: 1.0
Date: Tue, 14 Nov 2023 12:17:07 +0000
Message-ID: <dce66fffe88b68fdd6afc07e7d567039@example.com>
Content-Type: multipart/alternative; boundary=4Xwz4puW
--4Xwz4puW
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
[Laravel](http://localhost)
# Hello!
クリックしてログインする
ログイン: http://127.0.0.1:8000/login/1?expires=1699966027&signature=e32012a0cb68f52cdf6f4b91cff9782bc89c3f34e521330df905cfc8d1c2add7
〇〇アプリをご利用いただきありがとうございます!
Regards,
Laravel
If you're having trouble clicking the "ログイン" button, copy and paste the URL below
into your web browser: [http://127.0.0.1:8000/login/1?expires=1699966027&signature=e32012a0cb68f52cdf6f4b91cff9782bc89c3f34e521330df905cfc8d1c2add7](http://127.0.0.1:8000/login/1?expires=1699966027&signature=e32012a0cb68f52cdf6f4b91cff9782bc89c3f34e521330df905cfc8d1c2add7)
© 2023 Laravel. All rights reserved.
また、間違ったログインリンクでログインすると403エラーが出てることも確認します。
終わりに
パスワードなしでのログイン機能ができました。
有効期限ありの署名付きURLを生成してメールでユーザーに送信することはパスワードリセットや退会機能などにも活かせそうなので便利ですねー
Discussion