🦓

[Laravel Breeze]署名つきURLでログインする

に公開

はじめに

Laravel Breezeに署名つきURLを使ってパスワードなしのログイン機能を実装していきます。
署名付きURLは安全なリンクを生成し、一時的なアクセス許可を提供するために使われます。

https://laravel.com/docs/10.x/urls#signed-urls

環境

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にします。

.env
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

パスワードを要求する処理を削除する

ログインビューにあるパスワードの入力フォームを削除します。

resources/views/auth/login.blade.php
<!-- 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メソッドを使わないでメールアドレスで認証するロジックを追加します。

app/http/controllers/auth/AuthenticatedSessionController.php
<?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分以内にログインしてください。']);
    }
}

ルートを追加する

routes/auth.php
Route::get('login/{user}', [AuthenticatedSessionController::class, 'loginViaToken'])
       ->name('login.token')
       ->middleware('signed');

Laravelでは、middleware('signed')メソッドを使用して、ルートに「署名付き」ミドルウェアを適用します。signedミドルウェアは主に、署名されたパラメータを持つURLの完全性を検証するために使用されます。

署名が有効な場合、リクエストはルートのコントローラに進みます。署名が有効でない、または見つからない場合、Laravelは403エラーでリクエストを中止します。

https://laravel.com/docs/10.x/middleware

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メソッドを作成します。

app/http/controllers/auth/AuthenticatedSessionController.php
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.  

メーラーの名前を変えます。

.env
MAIL_MAILER=log

ログイン用リンクを生成します。

app/https/controllers/auth/authenticatedsessioncontroller.php
// Loginクラスに$linkを渡す
$user->notify(new Login($link));

メールの本文を作成します。

app/notifications/Login.php
<?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('〇〇アプリをご利用いただきありがとうございます!');
    }
}

https://laravel.com/docs/10.x/notifications

ログイン画面でメールアドレスを入力し、ログインをクリックします。
Laravelのログファイルでメールを送信されたことを確認します。
ログインリンクをクリックしてログインされたことを確認します。

storage/logs/laravel.log
[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