13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Laravel】メールアドレス+認証コードでモダンな認証機能を作成する

Last updated at Posted at 2023-09-24

本記事のゴール

画面収録 2023-09-25 7.13.16.gif

開発環境

  • M1 MacbookPro
  • macOS Venture13.5.1
  • Docker Deskctop

利用フレームワーク/パッケージ

  • Laravel Sail
  • Laravel 10
  • Laravel Breeze

実装フロー

  1. プロジェクトの初期設定
    1-1. プロジェクト作成
    1-2. エイリアスの設定
    1-3. phpMyAdminの導入
    1-4. 起動とパッケージインストール
    1-5. migrateの実行
    1-6. タイムゾーンの変更
    1-7. mysqlの設定

  2. テーブル設定・初期ルーティング
    2-1. migrationファイルを編集
    2-2. モデルファイルを修正
    2-3. welcomeページを修正
    2-4. bladeファイルを作成
    2-5. 初期ルーティング設定
    2-6. コントローラー設定

  3. メール送信のロジック作成
    3-1. Mailableクラスを作成
    3-2. コントローラーの編集

  4. ワンタイムトークン認証のロジック作成
    4-1. トークンの正当性チェック
    4-2. ルーティングを追加

1.プロジェクトの初期設定

Laravel Breezeのインストールまですでに終わっている方は
2-1. migrationファイルを編集こちらまでスキップしてください。

1-1.プロジェクト作成

任意の場所にプロジェクトを作成します。

cd dev
mkdir docker
cd docker

以下のコマンドでLaravelのプロジェクトを作成します

curl -s https://laravel.build/[任意のプロジェクト名] | bash

私はLaravel-Qiitaというプロジェクト名で作成しました。

curl -s https://laravel.build/laravel-qiita | bash

1-2.エイリアスの設定

すでに設定してある方は無視してください。

~/.zshrc
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'

1-3.phpmyadminの導入

プロジェクト内に移動しdocker-compose.ymlに追記します

/docker-compose.yml
services:
    laravel.test:
・・・省略
    phpmyadmin:
        image: phpmyadmin/phpmyadmin
        links:
            - mysql:mysql
        ports:
            - 8080:80
        environment:
            MYSQL_USER: "${DB_USERNAME}"
            MYSQL_PASSWORD: "${DB_PASSWORD}"
            PMA_HOST: mysql
        networks:
            - sail

1-4.起動とパッケージインストール

Sailの起動

sail up

これを実行しlocalhostにアクセスすると初期ページが表示されます。
スクリーンショット 2023-09-23 2.09.14.png

パッケージインストール

sail npm install

ユーザ認証パッケージにLaravel Breezeを使用します。

sail composer require laravel/breeze --dev
sail artisan breeze:install

バージョンによって色々聞かれるみたいですがすべてデフォルトで進めました。(参考までに↓)
スクリーンショット 2023-09-23 2.16.36.png

js/cssのビルド

sail npm run dev

5173ポートでviteが起動します
スクリーンショット 2023-09-23 2.33.24.png

1-5.migrateの実行

sail artisan migrate

1-6.タイムゾーンの変更

ワンタイムトークンの有効期限の設定のため、日本時間に変更します。

/config/app.php
'timezone' => 'Asia/Tokyo',

1-7.mysqlの設定

dbアクセスの際にエラーになる場合があるためstrictモードをオフにします。

/config/database.php
'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'prefix_indexes' => true,
            'strict' => false, //変更
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

2.テーブル設定・初期ルーティング

2-1.migrationファイルを編集

カラムの追加処理を追記します。

sail artisan make:migration add_onetime_token_to_users_table --table=users
/App/database/migrations/〇〇_add_onetime_token_to_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
    /**
     * Run the migrations.
     */
    public function up(): void {
        Schema::table('users', function (Blueprint $table) {
            $table->string('name')->default('未設定')->change(); // nameカラムのnullを許可する
            $table->string('password')->nullable()->change(); // passwordカラムのnullを許可する
            $table->char("onetime_token", 4)->nullable(); // ワンタイムトークン
            $table->dateTime("onetime_expiration")->nullable(); // ワンタイムトークンの有効期限
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void {
        Schema::table('users', function (Blueprint $table) {
            $table->string('name')->default()->change();
            $table->string('password')->nullable(false)->change();
            $table->dropColumn("onetime_token");
            $table->dropColumn("onetime_expiration");
        });
    }
};

実行しておきます。

sail artisan migrate

2-2.モデルファイルを修正

変更可能なカラムにワンタイムトークンとその有効期限を追加します。

/App/Models/User.php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable {
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
        'onetime_token', // 追加
        'onetime_expiration' // 追加
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
        'onetime_token', // 追加
        'onetime_expiration' // 追加
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

2-3.welcomeページを修正

ログイン画面へのリンクを追加します。

<body class="antialiased">
        <div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">         
            <div class="sm:fixed sm:top-0 sm:right-0 p-6 text-right z-10">
                @auth
                    <a href="{{ url('/dashboard') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Dashboard</a>
                @else
                    @if (Route::has('auth.first-auth'))
                        <a href="{{ route('auth.first-auth') }}" class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">ログイン / 新規登録</a>    
                    @endif
                @endauth
            </div>   

2-4.bladeファイルを作成

メール認証画面

/resources/views/auth/first-auth.blade.php
<x-guest-layout>
    <form method="POST" action="{{ route('sendTokenEmail') }}">
        @csrf

        <!-- Email Address -->
        <div class="mt-4">
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required/>
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">
            <x-primary-button class="ml-4">
                {{ __('認証コード送信') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

ワンタイムトークン認証画面

/resources/views/auth/second-auth.blade.php
<x-guest-layout>
    <form method="POST" action="{{ route('dashboard') }}">
        @csrf

        <!-- Email Address -->
        <div class="mt-4">
            <x-input-label for="onetime_token" :value="__('認証コード')" />
            <x-text-input id="onetime_token" class="block mt-1 w-full" type="number" name="onetime_token" :value="old('onetime_token')" required/>
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">
            <x-primary-button class="ml-4">
                {{ __('ログイン / 新規登録') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

2-5.初期ルーティング設定

一旦確認用にルーティングを設定します。

auth.php
Route::middleware('guest')->group(function () {
    // Route::get('register', [RegisteredUserController::class, 'create'])
    //     ->name('register');

    /**
     **最初のメール入力画面を表示するルーティング
     */
    Route::get('first-auth', [RegisteredUserController::class, 'create'])
        ->name('auth.first-auth'); // 追加

    /**
     **トークンを含んだメールを送信するルーティング
     */
    Route::post('sendTokenEmail', [RegisteredUserController::class, 'sendTokenEmail'])
        ->name('sendTokenEmail');
・・・省略
}

2-6.コントローラー設定

ひとまず画面を表示させるのみです。

/app/Http/Controllers/Auth/RegisteredUserController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;

class RegisteredUserController extends Controller {
    /**
     **最初の最初のメール入力画面を表示する
     */
    public function create(): View {
        return view('auth.first-auth');
    }

    /**
     **ワンタイムトークンが含まれるメールを送信する
     */
    public function sendTokenEmail(Request $request) {
        return view('auth.second-auth');
    }
・・・省略
});

ここまで設定するとこのような画面が表示されます。
画面収録 2023-09-25 3.53.22.gif
※撮影のため拡大してあります

3. メール送信のロジック作成

3-1.mailableクラスを作成

sail artisan make:mail TokenEmail 
/app/Mail/TokenEmail.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 TokenEmail extends Mailable {
    use Queueable, SerializesModels;
    private $email;
    private $onetime_token;

    /**
     * Create a new message instance.
     */
    public function __construct($email, $onetime_token) {
        $this->email = $email;
        $this->onetime_token = $onetime_token;
    }

    /**
     **メール作成
     */
    public function build() {
        return $this->to($this->email)
            ->subject("認証コード")
            ->view('auth.mail')
            ->with([
                'onetime_token' => $this->onetime_token
            ]);
    }
}

ついでにメール本文を表示するbladeファイルも作成しておきます

/resources/views/auth/mail.blade.php
<h2>{{$onetime_token}}</h2>

3-2.コントローラーの編集

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
use App\Mail\TokenEmail;
use Illuminate\Support\Facades\Mail;

class RegisteredUserController extends Controller {
    /**
     **最初の最初のメール入力画面を表示する
     */
    public function create(): View {
        return view('auth.first-auth');
    }

    /**
     **引数で渡されたメールアドレスとワンタイムトークンをusersテーブルに追加するコントロール
     */
    public static function storeEmailAndToken($email, $onetime_token, $onetime_expiration) {
        User::create([
            'email' => $email,
            'onetime_token' => $onetime_token,
            'ontime_expiration' => $onetime_expiration
        ]);
    }

    /**
     **引数で渡されたワンタイムトークンをusersテーブルに追加するコントロール
     */
    public static function storeToken($email, $onetime_token, $onetime_expiration) {
        User::where('email', $email)->update([
            'onetime_token' => $onetime_token,
            'onetime_expiration' => $onetime_expiration
        ]);
    }

    /**
     **ワンタイムトークンが含まれるメールを送信する
     */
    public function sendTokenEmail(Request $request) {
        $email = $request->email;
        $onetime_token = "";

        for ($i = 0; $i < 4; $i++) {
            $onetime_token .= strval(rand(0, 9)); // ワンタイムトークン
        }
        $onetime_expiration = now()->addMinute(3); // 有効期限

        $user = User::where('email', $email)->first(); // 受け取ったメールアドレスで検索
        if ($user === null) {
            RegisteredUserController::storeEmailAndToken($email, $onetime_token, $onetime_expiration);
        } else {
            RegisteredUserController::storeToken($email, $onetime_token, $onetime_expiration);
        }

        session()->flash('email', $email); // 認証処理で利用するために一時的に格納
    
        Mail::send(new TokenEmail($email, $onetime_token));

        return view("auth.second-auth");
    }
}

ここまでできると、first-authの画面で認証コード送信を押した際に
http://localhost:8025/にメールが届くようになります。

スクリーンショット 2023-09-25 6.12.08.png
スクリーンショット 2023-09-25 6.12.35.png
スクリーンショット 2023-09-25 6.13.15.png
スクリーンショット 2023-09-25 6.13.47.png

4.ワンタイムトークンの認証ロジック

ここまできたらもう少しです!

4-1.トークンの正当性チェック

/app/Http/Controllers/Auth/RegisteredUserController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
use App\Mail\TokenEmail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;

class RegisteredUserController extends Controller {
・・・省略
    /**
     **ワンタイムトークンが正しいか確かめてログインさせる
     */
    public function auth(Request $request): RedirectResponse {
        $user = User::where('email', session('email'))->first();
        $expiration = new Carbon($user['onetime_token']);

        if ($user['onetime_token'] == $request->onetime_token && $expiration > now()) {
            Auth::login($user);
            return redirect(RouteServiceProvider::HOME);
        }
        return redirect()->route('auth.first-auth');
    }
}

4-2.ルーティングを追加

/routes/auth.php
<?php

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;

Route::middleware('guest')->group(function () {
    /**
     **最初のメール入力画面を表示するルーティング
     */
    Route::get('first-auth', [RegisteredUserController::class, 'create'])
        ->name('auth.first-auth'); // 追加

    /**
     **トークンを含んだメールを送信するルーティング
     */
    Route::post('sendTokenEmail', [RegisteredUserController::class, 'sendTokenEmail'])
        ->name('sendTokenEmail');
    
    /**
     **ワンタイムトークンが正しいか確かめてログインさせるルーティング
     */
    Route::post('login', [RegisteredUserController::class, 'auth'])
        ->name('login'); // 追加
});
・・・省略

おわりに

とても長い記事となってしまいましたがご覧いただきありがとうございました。

今回はLaravel Breezeの認証機能を改造し、ワンタイムトークンによる認証を作成してみました。
参考にしていただくのは結構ですが、Breezeで用意されているものを変更して使っているため、セキュリティに関しては保証できかねます。

もし少しでも役に立った場合はぜひ♡をお願いします。

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?