LumenでJWT

# TL;DR

LumenでJWTを使ってみた。

# Lumenとは？

Laravelと同じ設計思想を持つマイクロフレームワーク。
Laravelをよりシンプルにしたものと考えればよい:thumbsup_tone1:

https://lumen.laravel.com

今回Lumenを選んだのは、JWTを用いたAPIのためにフレームワークを使いたいが、Laravelを使うほど大袈裟じゃないと思ったので。

# Lumenのインストール
Lumen 5.6をインストールしてみる。プロジェクト名は「lumen-api」で。

https://lumen.laravel.com/docs/5.6#installation

```console
$ composer create-project --prefer-dist laravel/lumen lumen-api
```

Valetを使っていればすぐに確認できる。
![スクリーンショット 2018-05-24 17.36.28.png](https://qiita-image-store.s3.amazonaws.com/0/123511/20dbbe0a-317e-6bc6-d747-8419344edb54.png)
デフォルト画面も非常にシンプル。

# .envのAPP_KEYが空

これはよろしくない。
Laravelだとインストールしたときにデフォルトでよしなにしてくれていたはずだけど。
しかも

```console
$ php artisan key:generate

  There are no commands defined in the "key" namespace.
```

うーむ。。。

https://stackoverflow.com/questions/30344141/lumen-micro-framework-php-artisan-keygenerate

を参考にして、

```console
$ php -r "require 'vendor/autoload.php'; echo str_random(32).PHP_EOL;"
```

の出力結果を`.env`のAPP_KEYに記述する。

# ファサードとEloquentを使えるようにする

これもLumenの導入記事でよく見かける。

```bootstrap/app.php
// $app->withFacades();

// $app->withEloquent();
```

のコメントアウトを外す。

# マイグレーション

簡単なAPIのためにとりあえずユーザテーブルを作る。

```console
$ php artisan make:auth

  Command "make:auth" is not defined.

  Did you mean one of these?
      make:migration
      make:seeder

```

こちらもまじか。。。:sob:
さすがシンプルを極めたフレームワーク。手厚いサポートは期待するなということなのでしょう。

```console
$ php artisan make:migration create_users_table
Created Migration: 2018_05_24_083938_create_users_table
```

```database/migrations/2018_05_24_083938_create_users_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

データベースを作って、`.env`にDB情報を記述したらマイグレーションを実行。

```console
$ php artisan migrate
Migration table created successfully.
Migrating: 2018_05_24_083938_create_users_table
Migrated:  2018_05_24_083938_create_users_table
```

# ユーザデータを作る

`make:auth`が使えればログイン画面や登録画面も同時に作られるのだが、いかんせんLumenは装備が少ない。
Tシャツ短パンで表参道を歩いているかのような気分だ(それでもお洒落な人はお洒落なのだけど)
ユーザの登録は今回の目的からずれてくるので、ここは一気にシーダーで作ってしまいます。
何故か既に`User.php`は用意されている。
`database/factories/ModelFactory.php`
を編集。

```database/factories/ModelFactory.php
$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => password_hash('pass', PASSWORD_DEFAULT),
    ];
});
```

続いて
`database/seeds/DatabaseSeeder.php`
をコピーした
`database/seeds/UsersTableSeeder.php`
を編集する。

```database/seeds/UsersTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class UsersTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(App\User::class, 10)->create();
    }
}
```

`database/seeds/DatabaseSeeder.php`
のコメントアウトを外す。

```database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call('UsersTableSeeder');
    }
}
```

シーダー実行。

```console
$ php artisan db:seed
Seeding: UsersTableSeeder

In Container.php line 767:

  Class UsersTableSeeder does not exist

$ composer dump-autoload
Generating optimized autoload files
$ php artisan db:seed
Seeding: UsersTableSeeder
```

失敗したので`composer dump-autoload`を実行しました。
確認するとusersテーブルにそれっぽいデータが登録されているはず。

ここまでで準備は完了。ここからJWTに移る。

# JWTとは？

https://jwt.io
https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6

JSON Web Token
認証に使うトークンのこと。
出来る限りステートレスにしたいということで、サーバサイドでのセッション管理をなくすために考え出されたものらしい。
認証の流れが以下のリンク先に画像としてある。

https://jwt.io/introduction/

認証を含むAPIを作る場合、このトークンを利用する認証がいちばん理に適っていると思います。
じゃあLaravelではLaravel Passportを使えばという気にもなるが、そこまでがっつりだとLumenを選択している意味もなくなりそうなので、ここは素のJWTを使ってみることにした。

https://jwt.io

には、それぞれの言語で使用出来るライブラリと、そのライブラリがどれくらいセキュアかの一覧がある。
その中で私が選んだのは、

https://github.com/lcobucci/jwt

すべてのチェックをクリアしていて、Auth0がサポートしているというのも信頼出来る。
まずはインストール。

```
composer require lcobucci/jwt
```

使い方は以下から。

https://github.com/lcobucci/jwt/blob/3.2/README.md

トークンを返す認証のエンドポイントを作る。

```routes/web.php
$router->post(
    'auth/login', 
    [
        'uses' => 'AuthController@authenticate'
    ]
);
```

```app/Http/Controllers/AuthController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\User;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;

class AuthController extends Controller
{
    private function jwt(User $user)
    {
        $signer = new Sha256();
        $token = (new Builder())->setIssuer('http://lumen-api.test')
            ->setAudience('http://lumen-api.test')
            ->setId(uniqid(), true)
            ->setIssuedAt(time())
            ->setNotBefore(time() + 60)
            ->setExpiration(time() + 3600)
            ->set('uid', $user->id)
            ->sign($signer, env('JWT_SECRET'))
            ->getToken();

        return $token;
    }

    public function authenticate(Request $request)
    {
        $this->validate($request, [
            'email'    => 'required|email',
            'password' => 'required'
        ]);

        $user = User::where('email', $request->input('email'))->first();

        if (!$user) {
            return response()->json(['error' => 'ログインできませんでした。'], 400);
        }

        if (Hash::check($request->input('password'), $user->password)) {
            return response()->json($this->jwt($user)->__toString(), 200);
        }

        return response()->json(['error' => 'ログインできませんでした。'], 400);
    }
}
```

また、署名用に`.env`にJWT_SECRETを追加する。

メールアドレスとパスワードでログインしてみる。

```console
$ curl -X POST -d "email=dummy@dummy-sample.com" -d "password=pass" http://lumen-api.test/auth/login
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjViMDY4N2Q5NzY0ZWUifQ.eyJpc3MiOiJodHRwOlwvXC9sdW1lbi1hcGkudGVzdCIsImF1ZCI6Imh0dHA6XC9cL2x1bWVuLWFwaS50ZXN0IiwianRpIjoiNWIwNjg3ZDk3NjRlZSIsImlhdCI6MTUyNzE1NDY0OSwibmJmIjoxNTI3MTU0NzA5LCJleHAiOjE1MjcxNTgyNDksInVpZCI6M30.Mbg0Xk_RaAKMTg8f64zfFPvCU-X3oiQdqlE658MqpRQ"
```

トークンが返ってきました。
トークンはピリオドで分かれた3ブロックからなる。順にヘッダー、ペイロード、署名となっている。

https://jwt.io

のDebuggerを使うとデコードしたデータも確認できる。
![スクリーンショット 2018-05-24 18.48.47.png](https://qiita-image-store.s3.amazonaws.com/0/123511/e65ffa98-1ac7-b939-0c0c-4251b776b82f.png)

# APIの作成

次は実際にこれでAPIで認証できるか確認してみる。

はじめにJWTのミドルウェアを作る。

```app/Http/Middleware/JwtMiddleware.php
<?php

namespace App\Http\Middleware;

use Closure;
use Exception;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use App\User;

class JwtMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $token = $request->get('token');

        if (!$token) {
            return response()->json([
                'error' => 'Token not provided.'
            ], 401);
        }

        $signer = new Sha256();
        $data = new ValidationData();
        $data->setIssuer('http://lumen-api.test');
        $data->setAudience('http://lumen-api.test');
        $data->setCurrentTime(time() + 60);

        try {
            $token = (new Parser())->parse((string) $token);

            if (!$token->validate($data)) {
                throw new Exception('バリデーションエラーです。');
            }
            if (!$token->verify($signer, env('JWT_SECRET'))) {
                throw new Exception('署名のエラーです。');
            }

            $user = User::findOrFail($token->getClaim('uid'));

        } catch (Exception $e) {
            return response()->json(['error' => $e->getMessage()], 400);
        }

        $request->user = $user;

        return $next($request);
    }
}
```

続いて、

```bootstrap/app.php
$app->routeMiddleware([
    'jwt.auth' => App\Http\Middleware\JwtMiddleware::class,
]);
```

```routes/web.php
<?php

$router->get('/', function () use ($router) {
    return $router->app->version();
});

$router->post(
    'auth/login', 
    [
        'uses' => 'AuthController@authenticate'
    ]
);

$router->group(
    ['prefix' => 'api', 'middleware' => 'jwt.auth'],
    function () use ($router) {
        $router->get('user', 'ApiController@user');
    }
);
```

トークンに含まれているユーザIDからデータを取得して返すAPIを作る。

```app/Http/Controllers/ApiController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ApiController extends Controller
{
    public function user(Request $request)
    {
        return response()->json($request->user, 200);
    }
}
```

```console
$ curl http://lumen-api.test/api/user?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjViMDY4YjljMjRmZDMifQ.eyJpc3MiOiJodHRwOlwvXC9sdW1lbi1hcGkudGVzdCIsImF1ZCI6Imh0dHA6XC9cL2x1bWVuLWFwaS50ZXN0IiwianRpIjoiNWIwNjhiOWMyNGZkMyIsImlhdCI6MTUyNzE1NTYxMiwibmJmIjoxNTI3MTU1NjcyLCJleHAiOjE1MjcxNTkyMTIsInVpZCI6M30.JxY1DWPnSGGMojcJm1lhuwHCAIL0L6I9yRbOZ9zGqrk
{"id":3,"name":"Cesar Schmidt","email”:”dummy@dummy-sample.com","created_at":"2018-05-24 08:49:24","updated_at":"2018-05-24 08:49:24"}
```
ミドルウェアによってトークン認証を行い、トークンに含まれているユーザIDでDBからデータを取得してきている。
不正なトークンだとエラーが返されるだろうか。

署名ブロックを書き換えた場合。

```console
$ curl http://lumen-api.test/api/user?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjViMDY4YjljMjRmZDMifQ.eyJpc3MiOiJodHRwOlwvXC9sdW1lbi1hcGkudGVzdCIsImF1ZCI6Imh0dHA6XC9cL2x1bWVuLWFwaS50ZXN0IiwianRpIjoiNWIwNjhiOWMyNGZkMyIsImlhdCI6MTUyNzE1NTYxMiwibmJmIjoxNTI3MTU1NjcyLCJleHAiOjE1MjcxNTkyMTIsInVpZCI6M30.JxY1DWPnSGGMojcJm1lhuwHCAIL0L6I9yRbOZ9zGqr
{"error":"署名のエラーです。"}
```

トークンの有効期限を過ぎた場合。

```console
$ curl http://lumen-api.test/api/user?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjViMDY5NDZiOGY2YTQifQ.eyJpc3MiOiJodHRwOlwvXC9sdW1lbi1hcGkudGVzdCIsImF1ZCI6Imh0dHA6XC9cL2x1bWVuLWFwaS50ZXN0IiwianRpIjoiNWIwNjk0NmI4ZjZhNCIsImlhdCI6MTUyNzE1Nzg2NywibmJmIjoxNTI3MTU3OTI3LCJleHAiOjE1MjcxNTc4NzcsInVpZCI6M30.FGV3ynPtUxcSXgzQYo9vy-Vcgyk6ZfrQS4ZWyNuZATo
{"error":"バリデーションエラーです。"}
```

エラーが返されることを確認できた。

実際に使う場合はもう少し工夫したほうがいいかもしれない。

# 参考

https://medium.com/tech-tajawal/jwt-authentication-for-lumen-5-6-2376fd38d454
ほぼ上記の記事の和訳みたいになってしまったぐらい助かった。感謝:pray_tone1:

https://lumen.laravel.com
https://stackoverflow.com/questions/30344141/lumen-micro-framework-php-artisan-keygenerate
https://jwt.io
https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6
https://github.com/lcobucci/jwt
https://github.com/lcobucci/jwt/blob/3.2/README.md
https://techblog.yahoo.co.jp/advent-calendar-2017/jwt/