【Laravel】API作成時のAuth(認証)機能カスタマイズ例まとめ


この記事は

[入江開発室アドベンドカレンダー2018](https://adventar.org/calendars/3128#list-2018-12-19)20日目の記事です。

前回は[aoki_tashipro](https://twitter.com/aoki_tashipro)さんの[入江開発室 体験談 劇的ビフォーアフター](https://coinbaby8.com/iriekaihatusitu-before-after.html)でした。



## 自己紹介

福岡の[Fusic](https://fusic.co.jp/)というWeb開発企業でエンジニアをしてる吉野です。
普段の業務ではCakePHPやVue.jsをやってます。
福岡で[Geek Studio](https://www.facebook.com/Geek-Studio-430121654073333/)というエンジニア、デザイナー向けのコワーキングスペースを運営してたりもします。


最近案件でLaravelを使う機会があり、[「入江開発室」](https://irie-dev.co/)にはLaravelを使う方が多いということもあり、今回Laravelの認証周りの記事を書くことにしました。



## php artisan make:auth




Laravelで認証機能を作成する際は

```bash

$ php artisan make:auth
```

このコマンドを叩くだけで、できてしまいます。
マイグレーションファイルまで作ってくれて、ほんと便利なコマンドですね。



ただ、このコマンド、APIを作成する際には色々とカスタマイズをする必要があります。

今回はそのカスタマイズ例を紹介していきます。



## 前提

```bash

$ php artisan make:auth
$ php artisan migrate
```

この二つのコマンドを実行した後、からスタートします。


## JWT認証を導入する


### 初期設定
今回はJWT認証を用いて、認証をカスタマイズしていきます。

JWT認証では、Laravel側で認証用のトークンを発行し、トークンがヘッダーに含まれているリクエストを認証済みユーザーとして認識する、という仕様になっています。


composerで[こちら](https://github.com/tymondesigns/jwt-auth)のプラグインをインストールします。

```
$ php composer.phar require tymon/jwt-auth 1.0.0-rc3
```


[jwt-authのドキュメント](https://jwt-auth.readthedocs.io/en/develop/laravel-installation/)に従って、初期設定をしていきます。


```bash

$ php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
$ php artisan jwt:secret
```




### Userモデルを編集

app/User.php

```php

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject; // 追加

class User extends Authenticatable implements JWTSubject // 追加
{
    use Notifiable;

    protected $fillable = [
        'username', 'email', 'password'
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

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

    // 追加
    public function getJWTCustomClaims()
    {
        return [];
    }

```

### config/auth.phpを修正

config/auth.php


```php

'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'jwt', // 変更
            'provider' => 'users',
        ],
    ],

```


## routes/api.phpを修正

```php
Route::group(["middleware" => "api"], function () {
// 認証が必要ないメソッド
    Route::group(['middleware' => ['jwt.auth']], function () {
    // 認証が必要なメソッド
    });
});
```

以上、jwtの初期設定になります。


## ユーザーの作成機能(Register)をつくる


app/Http/Controllers/Auth/RegisterController.php にユーザーの作成機能を書いていきます。



```php
// app/Http/Controllers/Auth/RegisterController.php内に追加

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\Events\Registered;

public function register(Request $request): JsonResponse
{
    $validate = $this->validator($request->all());

    if ($validate->fails()) {
        return new JsonResponse($validate->errors());
    }

    event(new Registered($user = $this->create($request->all())));

    return new JsonResponse($user);
}
```

また、routes/api.phpに以下を追記します。


```php
Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register'); // 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
    });
});
```


これでLaravel側のユーザー作成機能ができました。


curlコマンドで実際に動作確認をします。

```bash
$ curl -X POST localhost:8080/api/register -d name=yoshino -d password=yoshino -d password_confirmation=yoshino -d email=sample@example.com
{"name":"yoshino","email":"sample@example.com","updated_at":"2018-12-19 03:34:54","created_at":"2018-12-19 03:34:54","id":1}
```

作成したユーザーが返ってきます。


## ユーザーのログイン機能(Login)をつくる


次にユーザーのログイン機能をつくります。



app/Http/Controllers/Auth/AuthController.php にユーザーの作成機能を書いていきます。


```php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
// 追加
use Illuminate\Auth\AuthManager;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = '/home';
    private $authManager;　// 追加

    public function __construct(AuthManager $authManager)　// 追加
    {
        $this->authManager = $authManager;　 //　追加
        $this->middleware('guest')->except('logout');
    }

    //　追加
    public function login(Request $request): JsonResponse
    {
        $guard = $this->authManager->guard('api');
        $token = $guard->attempt([
            'email' =>  $request->get('email'),
            'password'  =>  $request->get('password'),
        ]);
        if (!$token) {
            return new JsonResponse(__('auth.failed'));
        }
        return new JsonResponse($token);
    }
}
```

ユーザー作成時と同様に、routesにも追加します。


```php

Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register');
    Route::post('/login', 'Auth\LoginController@login');　// 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
    });
});
```

これでログイン機能ができました。
curlでログインしてみます。


```bash
$ curl -X POST localhost:8080/api/login -d email=sample@example.com -d password=yoshino
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvYXBpXC9sb2dpbiIsImlhdCI6MTU0NTE5MjAwOSwiZXhwIjoxNTQ1MTk1NjA5LCJuYmYiOjE1NDUxOTIwMDksImp0aSI6IlR6R01zanB0ODNXam1nanIiLCJzdWIiOjIsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ.dhXhsCIrmfBuAyVRl1RSUuLJ5PAo9KMPepM5viqjaGw"
```


jwtトークンが返ってきます。



## ログイン済みユーザーだけがアクセスできるページを作成する

ログイン機能を作成したので、ログイン済みユーザーだけがアクセスできるAPIを作ってみましょう。


新しくApiController.phpを作成します。

app/Http/Controller/ApiController.php

```php
<?php

namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;

class ApiController extends Controller
{
    public function index()
    {
        return new JsonResponse('You are authorized user');
    }
}
```

ただJsonを返すだけのfunctionになります。


次にroutesに追記をします。

```php
Route::group(["middleware" => "api"], function () {
    Route::post('/register', 'Auth\RegisterController@register');
    Route::post('/login', 'Auth\LoginController@login');
    Route::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index');　// 追記
    });
});
```

この状態で、curlでアクセスします。

まず、ログインしない状態でアクセスしてみます。

```bash
$ curl -X GET localhost:8080/api/home
<!doctype html>
<html lang="en">
    <head>
        <title>Unauthorized</title>
```

DOMが表示されますが、「Unauthorized」のメッセージが返ってきているのがわかります。


次に先ほどログインした際に取得したトークンを、ヘッダーに入れた状態でアクセスします。

```bash
$ curl -X GET localhost:8080/api/home -H 'Authorization:Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MFwvYXBpXC9sb2dpbiIsImlhdCI6MTU0NTIzMTA4MiwiZXhwIjoxNTQ1MjM0NjgyLCJuYmYiOjE1NDUyMzEwODIsImp0aSI6ImhjWDJ4T2FxdFJXNlJoYzYiLCJzdWIiOjIsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ.zS0T3NqC_UM5xB-L8iN9zYPmbtHhm1C92fYSnj0d6L8'
"You are authorized user"
```

しっかりアクセスでき、メッセージが返ってきたのがわかります。

これでログイン済みユーザーだけがアクセスできるページが完成しました。




## パスワードリセット機能をカスタマイズする

次に、パスワードリセット機能をAPI用にカスタマイズしていきます。


### パスワードリセットメールを送信する

まずはメールアドレスを入力すると、指定されたメールアドレスに対し、パスワードリセット用のリンクが送られてくる、ところまでを作っていきます。


app/Http/Controller/Auth/ForgotPasswordController.php

```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));
    }
}
```

ここでは4つのfunctionを上書きして、API用に変更しています。


次にパスワードリセットメールを変更する必要があります。
既存のものだと、Laravel側のURLに遷移してしまうためです。

まず、メールの設定をします。

.envの以下の部分にお持ちのsmtpの設定を記述します。

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


次に、フロントエンド側のURLを定義してあげます。

config/frontend.phpを作成します。

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


次に、Userモデルに追記します。

app/User.php

```php

use App\Notifications\CustomPasswordReset; // 追記

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


CustomPasswordResetをつくっていきます。


app/Notifications/CustomPasswordReset.php

```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
            );
    }
}
```

最後にメールテンプレートを作成して、routesに追加します。

resources/views/emails/resetpass.blade.php

```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

```php

Route::group(["middleware" => "api"], 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::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index');
    });
});
```







これでパスワードリセットメールが送られてきます。

curlで試してみます。(実際試す際は、メールを受け取れるメールアドレスでお試しください)

```bash
$ curl -X POST http://localhost:8080/api/password/email -d email=sample@example.com
"Send Reset Mail"
```


メールを確認すると、以下のようなメールが届きます。



> Laravel

> Click link below and reset password.
> If you did not request a password reset, no further action is required.

> リセットパスワード: http://localhost:3000/reset?queryURL=http://localhost:8080/api/password/reset/a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7



これでパスワードリセットメールの受信ができました。



## パスワードリセットリンクから、新しいパスワードを入力して、パスワードを更新する


次に送信したリンク先から、パスワードの更新をします。


app/Http/Controller/Auth/ResetPasswordController.phpを編集していきます。

```php
<?php

namespace App\Http\Controllers\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
{
    use ResetsPasswords;
    protected $redirectTo = '/home';

    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',
        ]);
    }
}
```

これでパスワードのリセットができます。
先ほどメールで送られてきた、リセットリンクのトークンを使って、実際にパスワードリセットをしてみます。


```bash
$ curl -X POST http://localhost:8080/api/password/reset/a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7 -d email=yoshino@fusic.co.jp -d password=password -d password_confirmation=password -d token=a0bb4145b83134e86f1fd9824ea5710442a69dc1a4bdfaa23123523cb82b0ec7
"Password Reset"
```
パスワードがリセットされました。

実際にSPAなどで使用する際は、メールで送られてきたURLのqueryURLに対して、POSTすることによって、パスワードのリセットが可能です。




## メール認証機能をカスタマイズする


最後に、メールの認証機能をAPI用にカスタマイズします。


Laravelにはアカウント作成時にメールの認証をするための、インターフェイスが入っています。
しかし、標準のものだと、メールに記載されるリンクがLaravel側のURLであるため、API用に作成してあげる必要があります。

### 認証メールをカスタマイズ

まずはメール認証の際に送信されるメールをカスタマイズします。


Userモデルを編集します。

app/User.php

```php
<?php

namespace App;

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, MustVerifyEmail // 追加
{
    use Notifiable;

    protected $fillable = [
        'name', 'email', 'password',
    ];

    protected $hidden = [
        'password', 'remember_token',
    ];

    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);
    }
}
```

次に、VerifyEmailクラスを作成します。

app/Notifications/VerifyEmail.php

```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);
    }
}
```

フロントエンドのメール認証用のURLを定義します。

config/frontend.php

```php

<?php
return [
    'url' => env('FRONTEND_URL', 'http://localhost:8888'),
    'reset_pass_url' => env('RESET_PASS_URL', '/reset?queryURL='),
    'email_verify_url' => env('FRONTEND_EMAIL_VERIFY_URL', '/verify?queryURL='), // 追記
];
```

routesに追記します。


routes/api.php


```php
Route::group(["middleware" => "api"], 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'); // 追加
    Route::group(['middleware' => ['jwt.auth']], function () {
        Route::get('/home', 'ApiController@index');
    });
});
```


これでユーザー作成時に、認証用のメールが届くようになります。

例のごとく、curlコマンドで試してみます。

```bash
$ curl -X POST localhost:8080/api/register -d email=sample@example.com -d name=yoshino -d password=password -d password_confirmation=password
{"name":"yoshino","email":"sample@example.com","updated_at":"2018-12-19 23:12:12","created_at":"2018-12-19 23:12:12","id":4}%
```

ユーザー作成と同時に、メール認証用のメールがユーザーのメールアドレスあてに送信されます。



### 送付されたリンクでメールを認証する


次に、送付されたリンクに入り、メールの認証を完了します。


app/Http/Controllers/Auth/VerificationController.phpを修正します。

```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');
    }
}
```


これで、メールで送付されたリンクにアクセスすると、メールの認証が完了します。

curlコマンドで試します。


```bash
$ curl -X GET "http://localhost:8080/api/email/verify/6?expires=1545265816&signature=cd0969ca3204bc25ffae1176801d081bc2e3de23dd751f0c28f08df49605d1f5"
"Email Verified"
```


"Email Verified"の文字が返ってきて、DBを確認すると、Userの「email_verified_at」に日時が登録されます。


こちらも、実際にSPAなどで使用する際には、送られてきたメールのリンクの「queryURL」にGETでアクセスするようにしてください。







### まとめ

以上、長くなりましたが、LaravelのAPI用の認証カスタマイズでした。


Laravelは便利な機能がたくさん揃っていますので、なるべく既にある機能をカスタマイズすることで、より効率的な開発を進められるとよいかと思います。


最後まで読んでいただき、ありがとうございました。