Laravel/ui と Next.js で認証してみた

## やりたいこと
* `Next.js` をフロント側として、 `Laravel/ui` で認証したい。
* 一度ログインしたら、cookieに認証情報を持たせて楽したい。

Laravelの認証は、バージョン8.0から `Jetstream` などを推奨していますが、既存サービスとの兼ね合いから今回はこちらを選定します。

## どうやるか
`Laravel/ui` に `response()->json` で返却してくれる機能があるので基本はそれに乗っかってカスタマイズしていきます。
最初のログインは、`Next.js`側で用意したログインページに `email` `password` を入力、
cookie認証は、`Users`テーブルに`token`カラムを用意して`cookie`に付与した`token`とで判別する形で進めます。


## 前提
* `Laravel/ui` のインストールが完了している。
* `cors`の設定が済んでいる(`config/cors.php`, `Middleware/Cors.php`など)

## Laravel側の実装

まず、`LoginController` を見ていきましょう。

``` php:LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

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

ログイン機能は、traitである `AuthenticatesUsers` が賄っているので、
こちらに書かれているfunctionをオーバーライドしていきます。

 `LoginController` に以下を追加します。

``` php:LoginController.php
protected function validateLogin(Request $request)
{
    // cookieにtokenがあった場合は、通常のvalidationロジックを回避する
    if ($request->cookie('token')) {
        $authUser = UserModel::where('token', '=', $cookies['token'])
            ->first();
    }
    if (empty($authUser)) {
        $request->validate([
            $this->username() => 'required|string',
            'password'        => 'required|string|min:8',
        ]);
    }
}

// ログイン処理にemail, password認証だけでなく、cookie認証も追加
public function attemptLogin(Request $request)
{
    $attempted = $this->guard()->attempt(
        $this->credentials($request), $request->filled('remember')
    );
    if (! $attempted) {
        // 通常のemail,passwordログインがだめだった場合に、cookieでの認証を行う
        $authUser = UserModel::where('token', '=', $cookies['token'])
            ->first();
        }
        if (! empty($authUser)) {
            Auth::login($authUser);
            return true;
        } else {
            $attempted = false;
        }
    } 
}
    
// 認証後に新しいtokenを作成し、responseにcookieとして付与
public function authenticated($request, $user)
{
    // ハッシュ化のロジックは何でもOK
    $newToken = md5($user->token);
    $_res = UserModel::where('token', '=', $user->token)
        ->update(['token' => $newToken]);
    // 今あるcookieを削除
    setcookie('token');
    // 新しく発行したtokenでcookieを作成
    $cookie = Cookie::make('token', $newToken, 60*24, '/', '', true, true);
    if ($request->wantsJson()) {
            $response = response(['messages' => 'success', 200])->cookie($cookies);
            return $response;
    }
}

// ログアウトでcookieのtokenを削除
public function loggedOut(Request $request)
{
    setcookie('token', '', time() + 1, '/', '', true, true);
    if ($request->wantsJson()) {
        $response = response()->json(['message' => 'success'], 200)
            ->cookie('token', '', time() + 1, '/', '', true, true);
        return $response;
    }
}
```

`$request->headers` に `'Accept': 'application/json'` が設定してあると、
`$request->wantsJson()` が `true` になり、`各return responseロジック`に落ちます。

`headers`に設定していない場合は、通常のbladeで認証するロジックと同一の挙動になり、
`RouteServiceProvider:HOME`へリダイレクトされたりします。


## Next.js側の実装

`Next.js`側は大きく分けて２つの機能を実装します。

* email,password入力のログインページ
* cookieによるログイン処理


まずは、email,passwordを入力するログインページを作成し、 onSubmitで実行されるloginAuthを実装します。

``` ts
const LoginPage: NextPage<LoginProps> = (props) => {
    const router = useRouter();

    const loginAuth = async (e: any) => {
        e.preventDefault();
        const email = e.target.email.value;
        const password = e.target.password.value;

        const res = await fetchUserLogin(email, password);
        console.log(res.status);
        // ログイン出来た場合は、ステータスコード200が返ってくる
        if (res.status === 200) {
            return router.push('/logged');
        }
    };

    const fetchUserLogin = async (email: string, password: string) => {
        const res = await fetch(`${BACKEND_URL}/login`, {
            method: 'POST',
            mode: 'cors',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json; charset=utf8',
                // headersに Accept: application/json が無いと jsonResponse を返してくれない
                'Accept': 'application/json',
            },
            body: JSON.stringify({ email, password }),
        });
        return res;
    };

return (
        <div>
            <main className="">
                <div className="content">
                    <form onSubmit={loginAuth}>
                        <ul>
                            <li>
                                <span className="inputTitle">メールアドレス</span>
                                <input type="email" name="email" className="email" required />
                            </li>
                            <li>
                                <span className="inputTitle">パスワード</span>
                                <input
                                    type="password"
                                    name="password"
                                    className="password"
                                    required
                                    minLength={8}
                                />
                            </li>
                        </ul>
                        <button type="submit">ログイン</button>
                    </form>
                </div>
            </main>
        </div>
    );

}
```

cookieによるログインはこんな感じ。

``` typescript
export const fetchCookieLoginUser = async () => {
    const res = await fetch(`${BACKEND_URL}/login`, {
        method: 'POST',
        mode: 'cors',
        // credentialsをincludeにするとリクエストにcookieを含められる
        credentials: 'include',
        headers: {
            'Content-Type': 'application/json; charset=utf-8',
            'Accept': 'application/json',
        },
    });
    return res;
};
```

ログインに成功した後のページは、
`fetchCookieLoginUser`を使ってcookie認証し、
認証を通ったもののみ表示するようなロジックを組みましょう。

``` typescript
const res = await fetchCookieLoginUser();
if (res.status !== 200) {
    router.push('/login');
}
----
ログイン後の処理
----
```


これで認証は出来るようになっているはずです。
登録処理においても、今回のログイン処理と同様にオーバーライドしながら`response->json`を返すことで、実装することができます。