🔖

Laravelの標準パスワード再設定機能をAPIから叩いてみる

2021/08/09に公開

「車輪の再発明」という言葉があるように、フレームワークで事前に用意されている機能を使わずに自分でコードを書いて作ってしまうというものがあります。

Laravelであれば認証機能は標準で基本的に揃っています。プロ中のプロの人が書いて色んな人がチェックした上で出来上がっているコードなのでそれを使うのが一番良いのですが、
apiで認証機能を使うのってどうすればいいんだろう...と疑問におもったので試してみました。

今回はパスワード再設定編です。パスワード再設定メール送信とパスワード再設定を行います。

パスワード再設定メール送信apiを実装する

標準搭載のパスワード再設定機能を用いてパスワード再設定APIを実装します。

ForgotPasswordControllerを作成する

まずはパスワードリセット用のコントローラを用意します。

php artisan make:controller Api/Auth/ForgotPasswordController

ForgotPasswordControllerをこのように記述します。

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;

class ForgotPasswordController extends Controller
{
    use SendsPasswordResetEmails;

    public function sendResetLinkEmail(Request $request)
    {
        $this->validateEmail($request);

        $response = $this->broker()->sendResetLink(
            $request->only('email')
        );

        return $response == Password::RESET_LINK_SENT
            ? response()->json(['message' => 'パスワード再設定メールを送信しました', 'status' => true], 201)
            : response()->json(['message' => 'パスワード再設定メールを送信できませんでした。', 'status' => false], 401);
    }
}

ルーティングを定義する

Controllerができたので、そのControllerにアクセスするためのルーティングを定義します。

パスワード再設定メール送信するためのapiのルーティングについては、api.phpにこのように記述します。

Route::post('password/request', [ForgotPasswordController::class, 'sendResetLinkEmail']); // パスワード再設定メール送信
Route::post('password/reset', [ForgotPasswordController::class, 'resetPassword']); // パスワード再設定

パスワード再設定のビューの方は解説しないので、 resources/views/auth/reset.blade.php を各自作成してフロント側を実装してください。

パスワードリセットメールをカスタマイズする

php artisan make:notification PasswordResetNotification

人によって、メール内容は異なると思うので、その部分は随時変更してください
標準の ResetPassword を継承して新しくクラスを作ります。

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Auth\Notifications\ResetPassword;

class PasswordResetNotification extends ResetPassword
{
    use Queueable;

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        if (static::$toMailCallback) {
            return call_user_func(static::$toMailCallback, $notifiable, $this->token);
        }

        return (new MailMessage())
                    ->subject('パスワードリセット通知')
                    ->view('emails.password-reset', [
                        'reset_url' => url(config('app.url') . route('password.reset', ['token' => $this->token, 'email' => $notifiable->getEmailForPasswordReset()], false))
                    ]);
    }
}

メール本文用のbladeを作成する

resources/views/emails/password-reset.blade.php を作成します。
ココの本文は適当に書きますので、ご自身で書き換えてください。

パスワード再設定リンク<br>
{{ $reset_url }}<br>

user.phpのsendPasswordResetNotificationをオーバーライドする

標準のパスワード再設定機能では、 Controllerでパスワードリセットメール送信のバリデーションをした後に、 User.phpの sendPasswordResetNotification を呼び出してメールを送信します。標準で用意されているNotificationではなく自分で作成した PasswordResetNotification を呼び出したいので、オーバーライドして挙動を書き換えます。


    /**
     * Override to send for password reset notification.
     *
     * @param [type] $token
     * @return void
     */
    public function sendPasswordResetNotification($token)
    {
        $this->notify(new PasswordResetNotification($token));
    }

テストを書く

ここまででパスワード再設定メール送信apiは完成したので、テストを書いて処理を確認してみます。

僕はメソッドごとにテストのクラスを分けたい派なので、このようなフォルダ構成にしています、

php artisan make:test AuthController/SendPasswordResetTest
<?php

namespace Tests\Feature\AuthController;

use App\Models\User;
use App\Notifications\PasswordResetNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class SendPasswordResetTest extends TestCase
{
    use RefreshDatabase;
    use WithFaker;

    /**
     * パスワードリセットを実行する(成功)
     *
     * @return void
     */
    public function testResetPassword()
    {
        Notification::fake();

        $user = User::factory()->create();

        $params = ['email' => $user->email];
        $response = $this->post('/api/password/request', $params);
        $response->assertStatus(200);
        $this->assertTrue($response->decodeResponseJson()['success']);

        Notification::assertSentTo(
            [$user],
            PasswordResetNotification::class
        );
    }

     /**
     * パスワードリセットを実行する(失敗:DBには存在しないメールアドレス)
     *
     * @return void
     */
    public function testResetPasswordMissingUser()
    {
        Notification::fake();

        $user = User::factory()->create();

        $params = ['email' => $this->faker->email]; // 適当なメールアドレス
        $response = $this->post('/api/password/request', $params);
        $response->assertStatus(200);
        $this->assertFalse($response->decodeResponseJson()['success']);

        Notification::assertNothingSent();
    }
}

パスワード再設定apiを作成する

パスワード再設定メール送信ができたので、パスワード再設定APIも作っていきます。

ルーティングをする

Route::post('password/reset/{token}', [ResetPasswordController::class, 'resetPassword']);

ResetPasswordControllerを作る

php artisan make:controller Api/Auth/ResetPasswordController

Controllerの中身はこんな感じです。

<?php

namespace App\Http\Controllers\Api\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;

class ResetPasswordController extends Controller
{
    use ResetsPasswords;

    public function __construct()
    {
        $this->middleware('guest');
    }

    public function resetPassword()
    {
        $credentials = request()->validate([
            'email' => 'required|email',
            'token' => 'required|string',
            'password' => 'required|string|confirmed'
        ]);

        $reset_password_status = Password::reset($credentials, function ($user, $password) {
            $user->password = bcrypt($password);
            $user->save();
        });

        if ($reset_password_status == Password::INVALID_TOKEN) {
            return ['success' => false]; // トークンが異なる場合の処理
        }

        return ['success' => true];
    }
}

テストを作る

これでパスワード再設定処理ができたので、テストを書いてみます。
先程のPasswordResetTestに追記します。

<?php

namespace Tests\Feature\AuthController;

use App\Models\PasswordReset;
use App\Models\User;
use App\Notifications\PasswordResetNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class PasswordResetTest extends TestCase
{
    use RefreshDatabase;
    use WithFaker;

    /**
     * メールアドレスとトークンで認証を行う(成功)
     */
    public function testPasswordReset()
    {
        Notification::fake();

        $user = User::factory()->create();
        $token = $this->passwordRequest($user);

        // パスワードをリセットする
        $newPassword = $this->faker->word();
        $params = [
            'email' => $user->email,
            'token' => $token,
            'password' => $newPassword,
            'password_confirmation' => $newPassword

        ];
        $response = $this->post('/api/password/reset', $params);

        $response->assertStatus(200);

        $this->assertTrue($response->decodeResponseJson()['success']);
    }

    /**
     * トークンが異なる
     */
    public function testPasswordResetWithInvalidToken()
    {
        Notification::fake();

        $user = User::factory()->create();
        $token = $this->passwordRequest($user);

        // パスワードをリセットする
        $newPassword = $this->faker->word();
        $params = [
            'email' => $user->email,
            'token' => $this->faker->word(),
            'password' => $newPassword,
            'password_confirmation' => $newPassword

        ];
        $response = $this->post('/api/password/reset', $params);

        $response->assertStatus(200);

        $this->assertFalse($response->decodeResponseJson()['success']);
    }

    private function passwordRequest(User $user)
    {
        // パスワードリセットをリクエスト(トークンを作成・取得するため)
        $this->post('/api/password/email', [
            'email' => $user->email
        ]);

        // トークンを取得する
        $token = '';

        Notification::assertSentTo(
            $user,
            PasswordResetNotification::class,
            function ($notification, $channels) use ($user, &$token) {
                $token = $notification->token;
                return true;
            }
        );
        return $token;
    }
}

終わり

以上で、パスワード再設定メール送信とパスワード再設定のAPIができました。
駆け足で説明が足りない部分があるかもしれませんが、コメントの方で質問していただけたら答えますので、よろしくお願いいたします!

では、また。

Discussion