LaravelとFlutterで認証機能を持ったアプリを作る　①Laravel Passportを使ってAPI認証を実装する

## 全体概要
①Laravel Passportを使ってAPI認証を実装する（本記事）
②[FlutterアプリからLaravelへリクエストを送信する](https://qiita.com/atm_33/items/9ffc89caf1c2b6d6d661)


## はじめに


仕事でLaravelとFlutterを使ったアプリを作成したのですが、Flutterの場合はFirebase Authenticationを使用して認証機能を実装することが多いらしく、あまり他の記事が見当たりませんでした。

何とかこの記事を参考に実装したのですが結構大変だったので、備忘録として残していきたいと思います。

https://medium.com/swlh/authenticating-flutter-application-with-laravel-api-caea30abd57


## バージョン情報
LaravelとFlutterのバージョン情報は下記です。

```
// Laravel
$ php artisan --version
Xdebug: [Config] Invalid mode 'false' set for 'XDEBUG_MODE' environment variable, fall back to 'xdebug.mode' configuration setting (See: https://xdebug.org/docs/errors#CFG-C-ENVMODE)
Laravel Framework 9.24.0


// Flutter
$ flutter --version
Flutter 3.0.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 85684f9300 (7 weeks ago) • 2022-06-30 13:22:47 -0700
Engine • revision 6ba2af10bb
Tools • Dart 2.17.5 • DevTools 2.12.2
```

## サンプルプロジェクト

解説はいいからソースはよという方はサンプルプロジェクトを作成したのでこちらを参考にしてください。

https://github.com/atomu330/flutter-laravel-auth


## 全体の大枠

今回作成するLaravelとFlutterでの認証機能の大枠は下記になります。

1. Flutterアプリからログインまたは会員登録APIを叩く
2. Laravel側でユーザー情報を取得。その時に認証トークンを作成してFlutterアプリに返却
3. Flutterアプリ側で受け取った認証トークンをShared Preferenceに保存。以降Laravelへリクエストを送信する際はヘッダーに保存した認証トークン情報を追加してリクエスト
4. 認証トークンがヘッダーに含まれている場合はAuth::user()等の関数でユーザー情報が取得できるのでLarave側で認証に関する処理を書くことができるようになる

それでは実装していきましょう！


## Laravel側の設定

それでは早速Laravel側の実装から進めていきたいと思います。

### ローカル環境構築

まずはあまり本筋とは関係ありませんが、ローカル環境の構築です、

Laravelのローカル環境の構築はDockerを使います。
今回はLaravel Sailを使いました。

https://qiita.com/hinako_n/items/45a7232b0b0ed16bffc8

```
$ curl -s https://laravel.build/laravel-sail | bash
$ cd laravel-sail && ./vendor/bin/sail up
```

コマンド二発さくっとLaravelのデフォルトページを表示させましょう。
詳細はリンク先を参照いただければと思います。



### Laravel Passportの導入

次にLaravelをAPI認証サーバーとして使えるようにするためにLaravel Passportを導入していきます。


https://readouble.com/laravel/9.x/ja/passport.html


Laravel Passportについてですが、ご存知の方もいると思うのですがLaravel Passportと似たようなものでLaravel Sanctuamというパッケージがあります。

どちらを使うか悩みどころですが今回は下記理由でLaravel Passportを採用しました。

 - 英語の解説記事がLaravel Passportで実装している
 - Laravel SanctuamはLaravel Passportの縮小版みたいな感じなのでLaravel Passportを使っておけば間違いなさそう（Laravel Sanctuamでしかできないことはなさそう）
 - 下記記事でも似たような結論になっている

https://qiita.com/MasayaGoto/items/e81c48b2f0994607bd00


というわけで早速composer経由でLaravel Passportをインストールします。

```
// Laravel Passportのインストール
$ composer require laravel/passport

// ユーザーモデルの作成
$ php artisan migrate

// Laravel Passportを使うための準備
$ php artisan passport:install
```

上記コマンドを実行して下準備を完了したら`App\Models\Users.php`に`Laravel\Passport\HasApiTokens`を追加します。

```php: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; // 削除
use Laravel\Passport\HasApiTokens; // 追加

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


```

次に`App\Providers\AuthServiceProvider.php`のbootメソッド内でPassport::routesメソッドを呼び出すように修正します。

```php:App\Providers\AuthServiceProvider.php

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport; // 追加

class AuthServiceProvider extends ServiceProvider
{
・・・
    public function boot()
    {
        $this->registerPolicies();

        // 追加
        if (! $this->app->routesAreCached()) {
            Passport::routes();
        }
    }
・・・
}
```


最後に`config\auth.php`でapi認証ガードを定義します。

```php:config\auth.php
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    // 追加
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

```


ここまででLaravel Passportの導入は完了です。


## 認証系のメソッドを追加

認証に必要なログイン・ログアウト・会員登録のAPIを実装していきます。

名前などは何でもいいですが今回はこんな感じでapi.phpに追加しまいた

```route\api.php
// 追加
use App\Http\Controllers\AuthController;

// 追加
Route::group(['prefix' => 'auth'], function () {
    Route::controller(AuthController::class)->group(function () {
        Route::post('/login', 'login');
        Route::post('/register', 'register');
        Route::get('/logout', 'logout')->middleware('auth:api');
    });
});
```

```
// コントローラーの作成
$ php artisan make:controller AuthController

```


### 会員登録
```php:app\Http\AuthController.php

public function register(Request $request)
{
    // バリデーション
    $validator = Validator::make($request->all(), [
        'name' => 'required',
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8',
    ]);
    if ($validator->fails()) {
        $returnMessage = "";
        // 配列で渡ってくるメッセージを文字列にする
        foreach ($validator->errors()->toArray() as $key => $errorMessages) {
            foreach ($errorMessages as $errorMessage) {
                $returnMessage .= $errorMessage;
                $returnMessage .= "\n";
            }
        }
        return response()->json([
            'message' => $returnMessage,
        ], 401);
    }
    $input = $request->only(['name', 'email', 'password']);
    $input['password'] = Hash::make($input['password']);
    $user = User::create($input);
    // トークンの作成
    $token = $user->createToken('appToken')->accessToken;
    return response()->json([
        'token' => $token,
        'user' => $user
    ], 200);
}
```

ポイントは下記になります。

 - バリデーションで返却するメッセージを配列ではなく文字列にする
 - トークンを作成してユーザーのモデルと一緒に返却する

です。

バリデーションについてはflutterそのまま配列で返却してflutter側で制御してもいいのですが、今回は一旦Laravelで作成されるエラーメッセージを文字列に変換してFlutterアプリ側に返却しています。

そうすることでFlutter側は`message`に格納した値をそのまま表示すればよいので処理が簡単になります。

トークンはユーザーを識別するトークンで、後述しますがこのトークンをリクエストのヘッダー情報に含めてFlutter側からLaravel側にリクエストを投げることで、Laravel側で`Auth::user()`などの認証に関する便利な機能を使用することができます。

その他はWebアプリの実装と特に違いはありません。名前・メールアドレス・パスワードを受け取ってユーザー情報を作成する簡単な処理になります。

### ログイン

```php:app\Http\AuthController.php
public function login(Request $request)
{
    $email = $request->input("email");
    $password = $request->input("password");

    if (Auth::attempt(['email' => $email, 'password' => $password])) {
        $user = Auth::user();
        // トークンを作成
        $token = $user->createToken('appToken')->accessToken;
        return response()->json([
            'token' => $token,
            'user' => $user
        ], 200);
    } else {
        return response()->json([
            'message' => '入力されたメールアドレスとパスワードに誤りがあります。',
        ], 401);
    }
}

```

メールアドレスとパスワードを受け取って、一致する会員情報が存在するか確認し、その結果を返却するという一般的なログインの処理になります。

会員登録の時と同じく

 - 返却するエラーメッセージは文字列
 - トークンを作成して返却する

の２点がポイントになります。


### ログアウト
```php:app\Http\AuthController.php
public function logout(Request $res)
{
    if (Auth::check()) {
        // トークンの無効化
        $token = Auth::user()->token();
        $token->revoke();

        return response()->json([], 200);
    } else {
        return response()->json([
            'message' => '会員情報を取得できませんでした。',
        ], 401);
    }
}
```

ログアウトの時はトークンを`revoke()`で無効化します。
それ以外には特に気になるところはないかなと思います。

という訳で最終的にはこんな感じになります

```php:app\Http\AuthController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use Hash;
use Auth;
use Validator;
use Log;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validator = Validator::make($request->all(), [
            'name' => 'required',
            'email' => 'required|email|unique:users',
            'password' => 'required|min:8',
        ]);
        if ($validator->fails()) {
            $returnMessage = "";
            foreach ($validator->errors()->toArray() as $key => $errorMessages) {
                foreach ($errorMessages as $errorMessage) {
                    $returnMessage .= $errorMessage;
                    $returnMessage .= "\n";
                }
            }
            return response()->json([
                'message' => $returnMessage,
            ], 401);
        }
        $input = $request->only(['name', 'email', 'password']);
        $input['password'] = Hash::make($input['password']);
        $user = User::create($input);
        $token = $user->createToken('appToken')->accessToken;
        return response()->json([
            'token' => $token,
            'user' => $user
        ], 200);
    }

    public function login(Request $request)
    {
        $email = $request->input("email");
        $password = $request->input("password");
        Log::info($email);
        Log::info($password);
        if (Auth::attempt(['email' => $email, 'password' => $password])) {
            $user = Auth::user();
            $token = $user->createToken('appToken')->accessToken;
            return response()->json([
                'token' => $token,
                'user' => $user
            ], 200);
        } else {
            return response()->json([
                'message' => '入力されたメールアドレスとパスワードに誤りがあります。',
            ], 401);
        }
    }

    public function logout(Request $res)
    {
        if (Auth::check()) {
            $token = Auth::user()->token();
            $token->revoke();

            return response()->json([], 200);
        } else {
            return response()->json([
                'message' => '会員情報を取得できませんでした。',
            ], 401);
        }
    }
}

```

ここまででLaravel側の実装は完了です！

## 一旦ここまで

というわけでまずは全体の構成とLaravel Passportを使ったLaravel側の実装についてでした。

次回でFlutterアプリ側の実装を完結させて実際に動くものを作っていきますので、そちらも見てもらえると嬉しいです。

https://qiita.com/atm_33/items/9ffc89caf1c2b6d6d661


ありがとうございました！