はじめに

システムの内部連携のためにJWTトークンによる認証を行う必要があるとのことで、その導入をおこないました。
その経緯や技術選定の際に考慮した点について共有したいと思います。

そもそもJWTトークンとは?

エンジニアの方ならすでにご存じの方が多いと思います。
ただ、前置きとして簡潔に概要を振り返りたいと思います。

よくあるログインの認証の仕組み

JWT以外の認証だとセッションベースの認証が多いかと思います。
セッションベースはユーザーがログインした場合アプリケーション側がログイン状態を覚えておき、再度サイトに訪れたときは、ログイン処理をスキップするというものです。
ユーザー(ブラウザ)にはアプリケーションから発行されたセッション変数がCookieとして格納され、ユーザーがサイト訪問した際にそのCookieをアプリケーションに渡してもらうことで アプリケーションはそのユーザーがログインした状態なのかをRedisなどのミドルウェアに格納された値を参照してチェックします。

JWTは何が違うか?

セッションベースと大きく異なるのは、アプリケーション側がユーザーのログイン状態を保持しないことです。
どういうことか?
ユーザー(ブラウザ)から渡されたCookieのトークンを検証して、問題なければ認証するというところは同じですが、保持しないという点に注目してください。
まずログイン認証が行われた際アプリケーション側の秘密鍵で生成された「トークン」をユーザーに返します(CookieやHTTPヘッダに格納されるケースが多いです)。
このときアプリケーションはその値をサーバー側に保持しません(ただし、ログアウトや失効管理のために保持する実装もあります)。
次にユーザーが再度訪問した際は、Cookieに格納されたJWTトークンが正常のものかを検証します。
具体的には、JWTトークンに含まれる署名をサーバー側の秘密鍵(または公開鍵)で検証できるため、Redisなどを参照する必要がありません。、Redisなどを参照する必要はないのです。
(もちろん、各システムの実装によりますので参考程度に。)

JWTトークンの構成

3つのパーツからできております。

  • ヘッダ
  • ペイロード
  • 署名

上記要素を下記のように「.」で繋いだものがJWTトークンとなります。
それぞれのパーツが各自base64エンコードされてます。

{hogehoge}.{fugaguga}.{hogefuga}
//ヘッダ    //ペイロード  //署名

ヘッダ
alg: 署名のアルゴリズム
typ: トークンタイプで「JWT」固定
下記jsonをbase64エンコードする

{
  "alg": "HS256",
  "typ": "JWT"
}

署名
上記2つ(ヘッダ&ペイロード)がエンコードされた文字列({xxxx}.{xxxxx})をヘッダのアルゴリズムと秘密鍵で署名し、さらにエンコードを行います。

JWTトークンをLaravelに導入する

ここまででカンの良い方なら分かると思いますが、そこまで複雑ではなく自前で実装できる範囲です。
ただし、JWTはインターネット標準仕様(RFC 7519)で定義されています。
この仕様を遵守できるようにするため、ライブラリ(tymon/jwt-auth)を選択しました。
今回はJWTトークンの生成と検証を実装範囲として、ミドルウェアでの認証への導入までは行いません。

パッケージ導入 & 設定ファイルの生成

composer require tymon/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
# .envにJWT_SECRETが生成されます。config/jwt.phpを編集すれば別の環境変数に設定することも可能です。

サンプルコード

JWTトークンの生成

    public function create(array $param): string
    {
        try {
            $customClaims = [
                'param01' => $param[0],
                'param02' => $param[1],
            ];

            $payload = JWTFactory::customClaims($customClaims)->make();
            $manager = $this->jwtAuth->manager();
            return $manager->encode($payload);

        } catch (\Exception $e) {
            // エラーハンドリング
        }
    }

JWTトークンの検証 ※booleanで判定結果を返す

    public function verify(
        string $token,
        array $param
    ): bool {
        try {
            $payload = $this->jwtAuth->setToken($token)->getPayload();

            if ((int) $payload->get('param01') !== $param[0]) {
                // 値が一致しない場合は無効とみなす
                return false;
            }
            if ((int) $payload->get('param02') !== $param[1]) {
                // 値が一致しない場合は無効とみなす
                return false;
            }

            return true;
        } catch (\Exception $e) {
            // エラーハンドリング
        }
    }

JWTトークンの生成、復号化してのチェックなら上記のように簡潔に実装することが可能です。

まとめ

今はJWTトークンが使われることが多くなってきています。
知識はあるけれど、実装したことが無いという方にとって本記事が参考になれば幸いです。
自分の場合は、理解しながら進めたため実装に数日かかりました。
自前で実装するもライブラリ使うのもどっちでも良いかと思いますが、意外とtymon/jwt-authで生成&検証するだけのサンプルコードがなかったので、用意してみました。
またなにか発見があれば、引き続き執筆しようと思います。