モダンな技術を全く知らないSIer5年目がWebサービスを作ることになったため0から勉強する ~Laravel jwt-auth + メール認証 + パスワードリセット~

# はじめに
現在SIer5年目でjavascript(Jqueryのみ)、PHP(フレームワーク無し)を2年ほど、C#(Windowsアプリ)3年ほどやってきました。
色々なご縁があり、個人で最近Webサービスの立ち上げをやることになったのですが何せ本当にWebサービスを立ち上げるための知識がほぼ0に等しいです:sob:

ただ今後のキャリアを考えた時に今のままではいけないと思いチャレンジすることにしました。

まずは最初に技術を習得しないといけないので、学ぶ＆アウトプットするために毎回投稿していこうと思います。
今後身についていこうと思ってるのは下記のような技術です。
**AWS**
**Docker**
**CI/CD環境の構築**
**Laravel**
**Nuxt.js**
今回はLaravel+Nuxtについて学んでいきます。

# 今回学ぶこと
Laravel＋Nuxtでのメール認証機能とパスワードリセット機能を作成していこうかと思います。
今回はLaravel API側の実装をしていきます

# 参考サイト
[jwt-auth公式サイト](https://jwt-auth.readthedocs.io/en/develop/)

[こちらのサイトを参考にさせていただきました](https://qiita.com/ayasamind/items/abadb9737e9d6480806b)

[メール認証の概要](https://qiita.com/yamaji_daisuke/items/731868a4de6037794976)は個々がわかりやっすかったです。

# 前提
Laravel 5.8
Nuxt　2.5.4 
jwt-auth

[ログイン機能など](https://qiita.com/gemetasu1103/items/cc41e65dff7824a63bef)はこちらで実装してるのでこちらを参照ください

[最終的なソースコードはこちらになります。](https://github.com/hajime1103/nuxt-laravel-jwt-auth-example)

# パスワードリセット機能の実装

`php artisan make:auth`で作成された`ForgotPasswordController`をAPI用にカスタマイズしていきます

```/app/Http/Controllers/api/Auth/ForgotPasswordController.php
<?php

namespace App\Http\Controllers\Auth;

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

class ForgotPasswordController extends Controller
{
    use SendsPasswordResetEmails;

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

    public function sendResetLinkEmail(Request $request)
    {
        //メールリセットのリクエストをチェックする
        $validate = $this->validateEmail($request->all());

        if ($validate->fails()) {
            return new JsonResponse('Email is Invalid');
        }

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

        return $response == Password::RESET_LINK_SENT
            ? $this->sendResetLinkResponse($request, $response)
            : $this->sendResetLinkFailedResponse($request, $response);
    }

    //メールアドレスチェック
    protected function validateEmail(array $data)
    {
        return Validator::make($data, [
            'email' => 'required|email',
        ]);
    }

    protected function sendResetLinkResponse(): JsonResponse
    {
        return new JsonResponse('Send Reset Mail');
    }

    protected function sendResetLinkFailedResponse(Request $request, $response): JsonResponse
    {
        return new JsonResponse(trans($response));
    }
}

```

Nuxt側でのパスワードリセットURLを指定する

```/config/frontend.php
<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:3000'), //フロントエンドのURL
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='), // フロントエンドのパスワードリセットページのURL
];

```

パスワードリセットのメールを呼び出す関数`sendPasswordResetNotification`を追加する

```/app/Http/Controllers/Models/User.php
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
+ use App\Notifications\CustomPasswordReset;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

+    public function sendPasswordResetNotification($token)
+    {
+        $this->notify(new CustomPasswordReset($token));
+    }
}

```

パスワードリセットメール通知用のclassを実装する

```app/Notifications/CustomPasswordReset.php
<?php

namespace App\Notifications;

use App;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class CustomPasswordReset extends Notification
{
    use Queueable;
    public $token;

    public function __construct($token)
    {
        $this->token = $token;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('パスワードリセット') // 件名
            ->view('emails.resetpass') // メールテンプレートの指定
            ->action('リセットパスワード',
                config('frontend.url') . config('frontend.reset_pass_url') .
                url('api/password/reset', $this->token) //アクセスするURL
            );
    }
}
```

メールテンプレートの作成を行う


```/resources/views/emails/resetpass.blade.php
<h3>
    <a href="{{ config('app.url') }}">{{ config('app.name') }}</a>
</h3>
<p>
    {{ __('Click link below and reset password.') }}<br>
    {{ __('If you did not request a password reset, no further action is required.') }}
</p>
<p>
    {{ $actionText }}: <a href="{{ $actionUrl }}">{{ $actionUrl }}</a>
</p>
```

ルートの定義を追加する

```/routes/api.php
Route::group(['prefix' => '/auth', ['middleware' => 'throttle:20,5']], function (){
   Route::post('/register', 'Auth\RegisterController@register');
   Route::post('/login', 'Auth\LoginController@login');
+   Route::post("/password/email", "Auth\ForgotPasswordController@sendResetLinkEmail"); 
+   Route::post("/password/reset/{token}", "Auth\ResetPasswordController@reset"); 
});
```


## メール送信テスト

今回は、[mailtrap](https://mailtrap.io/)というサービスを使用して簡単にローカルでもメール送信テストをしていきたいと思います

[ここを参考に構築しました](https://qiita.com/ubonsa/items/5514fb9c5d5783bcc758)

`.env`参考サイトを元に記載しました。

```env
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=XXXXX
MAIL_PASSWORD=XXXXX
MAIL_ENCRYPTION=null
```

PostManでテストする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305479/229ba955-faac-479f-cf07-e078d867f0b3.png)

mailtrapでも送信ができていることを確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305479/f67fc494-16f5-7c13-6dd0-1ebf11d4df43.png)

## パスワードリセットするリクエストを実行するAPIを実装

```/app/Http/Controller/api/Auth/ResetPasswordController.php
<?php

namespace App\Http\Controllers\api\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
+ use Illuminate\Http\Request;
+ use Illuminate\Support\Str;
+ use Illuminate\Support\Facades\Validator;
+ use Illuminate\Support\Facades\Password;
+ use Illuminate\Auth\Events\PasswordReset;
+ use Illuminate\Contracts\Auth\PasswordBroker;
+ use Illuminate\Http\JsonResponse;

class ResetPasswordController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Password Reset Controller
    |--------------------------------------------------------------------------
    |
    | This controller is responsible for handling password reset requests
    | and uses a simple trait to include this behavior. You're free to
    | explore this trait and override any methods you wish to tweak.
    |
    */

    use ResetsPasswords;

    /**
     * Where to redirect users after resetting their password.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

+     public function reset(Request $request)
+     {
+         $validate = $this->validator($request->all());
+ 
+         if ($validate->fails()) {
+             return new JsonResponse($validate->errors());
+         }
+         $response = $this->broker()->reset(
+             $this->credentials($request), function ($user, $password) {
+             $this->resetPassword($user, $password);
+         }
+         );

+         return $response == Password::PASSWORD_RESET
+             ? $this->sendResetResponse($request, $response)
+             : $this->sendResetFailedResponse($request, $response);
+     }

+     protected function resetPassword($user, $password)
+     {
+         $user->forceFill([
+             'password' => bcrypt($password),
+             'remember_token' => Str::random(60),
+         ])->save();
+     }

+     protected function sendResetResponse(Request $request, $response)
+     {
+         return new JsonResponse('Password Reset');
+     }

+     protected function sendResetFailedResponse(Request $request, $response)
+     {
+         return new JsonResponse($response);
+     }

+     protected function validator(array $data)
+     {
+         return Validator::make($data, [
+             'token' => 'required',
+             'email' => 'required|email',
+             'password' => 'required|confirmed|min:6',
+         ]);
+     }
+ }

```

PostManでパスワードが変更できるのか確認する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305479/8d186860-798a-951b-0060-6a52bb7f7005.png)

# メール認証機能の実装



```/app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
use App\Notifications\CustomPasswordReset;
+ use App\Notifications\VerifyEmail;


- class User extends Authenticatable implements JWTSubject
+ class User extends Authenticatable implements JWTSubject, MustVerifyEmail
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    public function getJWTCustomClaims()
    {
        return [];
    }

    public function sendPasswordResetNotification($token)
    {
        $this->notify(new CustomPasswordReset($token));
    }

+     public function sendEmailVerificationNotification()
+     {
+         $this->notify(new VerifyEmail);
+     }
}

```

メール認証のURLを生成する`VerifyEmail`を作成する

```/app/Notifications/VerifyEmail.php
<?php

namespace app\Notifications;

use App;
use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;
use Illuminate\Support\Facades\URL;
use Carbon\Carbon;

class VerifyEmail extends VerifyEmailBase
{
    protected function verificationUrl($user)
    {
        $prefix = config('frontend.url') .config('frontend.email_verify_url');
        $routeName = 'verification.verify';
        $temporarySignedURL = URL::temporarySignedRoute(
            $routeName, Carbon::now()->addMinutes(60), ['id' => $user->getKey()]
        );

        return $prefix . urlencode($temporarySignedURL);
    }
}
```

Nuxt側の認証画面URLを定義する

```/config/frontend.php
<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:3000'), //フロントエンドのURL
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='), // フロントエンドのパスワードリセットページのURL
+     'email_verify_url' => env('FRONTEND_EMAIL_VERIFY_URL', '/verify?queryURL='), // メール認証のURL
];
```

認証APIのルートも定義する

```/routes/api.php
Route::group(['prefix' => '/auth', ['middleware' => 'throttle:20,5']], function (){
   Route::post('/register', 'Auth\RegisterController@register');
   Route::post('/login', 'Auth\LoginController@login');
   Route::post("/password/email", "Auth\ForgotPasswordController@sendResetLinkEmail");
   Route::post("/password/reset/{token}", "Auth\ResetPasswordController@reset");
+    Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify');
+    Route::post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
});
```

ユーザ登録時にメール認証のためのメール送信を行うために`RegisterController`を修正する

```/app/Http/Controllers/Auth/RegisterController.php 
- $user = $this->create($request->all());
+ event(new Registered($user = $this->create($request->all())));

```

ユーザ登録を行い、メールが送られてることを確認する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305479/83798f51-b97e-6b6f-ffca-0c67b75fa96a.png)

認証を完了するAPIを実装する

```/app/Http/Controllers/api/Auth/VerificationController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Http\Request;
use Illuminate\Auth\Events\Verified;
use App\User;
use Illuminate\Http\JsonResponse;

class VerificationController extends Controller
{
    use VerifiesEmails;

    public function __construct()
    {
        $this->middleware('throttle:6,1');
    }

    public function verify(Request $request)
    {
        $user = User::find($request->route('id'));
        if (!$user->email_verified_at) {
            $user->markEmailAsVerified();
            event(new Verified($user));
            return new JsonResponse('Email Verified');
        }
        return new JsonResponse('Email Verify Failed');
    }

    public function resend(Request $request)
    {
        $user = User::where('email', $request->get('email'))->get()->first();
        if (!$user) {
            return new JsonResponse('No Such User');
        }
        if ($user->hasVerifiedEmail()) {
            return new JsonResponse('Already Verified User');
        }

        $user->sendEmailVerificationNotification();

        return new JsonResponse('Send Verify Email');
    }
}
```

認証用のURLを叩いて、DBの`email_verified_at`が日時が入っていれば認証完了となる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305479/a0586e14-e4aa-771b-4601-7bf2c70d3675.png)

# まとめ
基本的には、Laravelの機能をAPI用に改造するだけで実装ができてしまうので、本当に便利でした。

次回はNuxtの画面側の実装もしていきたいと思います。