【JWT】JWTとは？〜PHPライブラリでJWTを使ってみる

# JWTとは
- Json Web Tokenの略称、ジョットと読む
- JSON形式のデータをURLセーフな方法で送信するための技術
- 電子署名によりデータの改ざんを検知でき、ユーザー認証などで利用されている
- 仕様は[RFC7519](https://datatracker.ietf.org/doc/html/rfc7519)に定められている

auth0のJWT特設サイトにサンプルとデバッガが用意されています。

https://jwt.io/

# JWTの構成
下記の要素をピリオドで連結した文字列です。
- Base64URLエンコードされたヘッダー
- Base64URLエンコードされたペイロード
- 署名

[auth0のJWT特設サイト](https://jwt.io/)より

```bash
# Base64URLエンコードされたペイロード.
# Base64URLエンコードされたペイロード.
# 署名
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.
tRIxJAB-GnSnpLRyw5YMZcXAdFG2d4pbc0pkzD-FqW6SufaVb6l7D7Fh-HIqzNpBiiIZM828uB6fKqY_SR1Jx8akV9RvN53kirNRtABhPNfaVehV3CMqU9Nn2jHU1x8WLmiEeF8f97CzTZ7WLgfIx1b44mzQkVzxcTBdDhiOkgHJO-HpOY1javKV0JgajEWFY_wq80nnHDlk_HdB8SLN3AhPvwsMPGMeIW0ICQoekNxqM_4HX_QKPf9Url2EY9V0TenrfL3W5arkBbgTk-GEUchLNrLa3LhxITY1TrLVkPv17MLbYcp3nip2p-_7Mz5hjY94rLkltqBfxv6nKyWN1g
```

## ヘッダー(header)
署名の生成に利用したアルゴリズムの情報です。
通常は``alg``と``typ``の2つで構成されます。

### alg(algolosm)
- 署名アルゴリズム
- HS256、RS256、ES256など

### typ(type)
- トークンのタイプ
- JWT一択

## ペイロード(payload)
データ本体です。
``クレーム(Claim)``と呼ばれるフィールドと値が対でセットされています。クレームは下記3種類に分けられます。

https://datatracker.ietf.org/doc/html/rfc7519#section-4
### 登録済みクレーム(Registered Claim)
- 標準のクレーム
- 利用が推奨されているが、必須ではない
- 詳細は[こちら](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1)

### パブリッククレーム(Public Claim)
- [IANA JSON Web Token Registry](https://www.iana.org/assignments/jwt/jwt.xhtml)に登録されている登録済みクレーム以外のクレーム

### プライベートクレーム(Private Claim)
- 登録済みクレーム、パブリッククレーム以外のクレーム
- クレームの名前は使用者が自由に設定できる

## 署名(signature)
下記の要素を``秘密鍵``を使用し``指定したアルゴリズム(alg)``で暗号化した文字列です。
- Base64URLエンコードされたヘッダー
- Base64URLでエンコードされたペイロード

# サンプルを読んでみよう
[auth0のJWT特設サイト](https://jwt.io/)のデバッガでRS256を指定した時に表示されるサンプルです。
上記の説明を踏まえるとトークンの構成がよく分かります。

```bash
# ヘッダー
{
  "alg": "RS256",      # 署名アルゴリズム
  "typ": "JWT"         # トークンのタイプ
}

# ペイロード
{
  "sub": "1234567890", # 登録済みクレーム
  "name": "John Doe",  # パブリッククレーム
  "admin": true,       # プライベートクレーム
  "iat": 1516239022    # 登録済みクレーム
}
```

# PHPライブラリでJWTを使ってみよう
PHPでJWTを使ってみます。
今回は``firebase/php-jwt``と``lcobucci/jwt``の2つのライブラリでトークンの生成・検証・データ取得処理を実装しました。

使用感も書きましたので実装の参考にしてください。

## firebase/php-jwt
https://github.com/firebase/php-jwt
- 「php jwt」のGoogle検索でトップ、情報量が多い
- シンプルな構成でキャッチアップが楽
- シンプルな反面、細かな検証ルールを追加すると実装方法が人それぞれになりやすい

```php
require_once '../vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class FirebaseJWT
{
    private string $signer;
    private string $private_key;
    private string $public_key;

    public function __construct()
    {
        $this->signer = 'RS256';
        $this->private_key = file_get_contents('private.pem');
        $this->public_key = file_get_contents('public.pem');
    }

    // tokenの生成
    public function createToken(): string
    {
        $payload = [
            'iss' => 'hogehoge',
            'sub' => 'Hello!',
            'name' => 'fugafuga',
            'admin' => true,
            'iat' => time()
        ];

        return JWT::encode($payload, $this->private_key, $this->signer);
    }

    // tokenの検証
    public function checkToken(string $jwt): bool
    {
        try {
            // 署名検証
            $decoded = JWT::decode($jwt, new Key($this->public_key, $this->signer));
            // iss検証
            if ($decoded->iss !== 'hogehoge') {
                throw new Exception('iss error');
            }
            // sub検証
            if ($decoded->sub !== 'Hello!') {
                throw new Exception('sub error');
            }
        } catch (Exception $e) {
            return false;
        }

        return true;
    }

    // tokenからデータを取得
    public function getData(string $jwt): array
    {
        return (array)JWT::decode($jwt, new Key($this->public_key, $this->signer));
    }
}

$firebase_jwt = new FirebaseJWT();
$jwt = $firebase_jwt->createToken();
$firebase_jwt->checkToken($jwt);
$firebase_jwt->getData($jwt);
```

## lcobucci/jwt
https://github.com/lcobucci/jwt
- 鍵の参照ロジック、署名やパブリッククレームの検証ロジックがクラス化されている
- 検証ロジックの集合をバリデーションルールとして一括で扱うことができる
- オブジェクト指向が好きな人向き
- 実装方法が固定されやすい
- キャッチアップにやや時間がかかる
```php
require_once '../vendor/autoload.php';

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\RSA\Sha256;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\IssuedBy;
use Lcobucci\JWT\Validation\Constraint\RelatedTo;

class LcobucciJWT
{
    private Configuration $config;

    public function __construct()
    {
        $this->config = Configuration::forAsymmetricSigner(
            new Sha256(),
            InMemory::file('private.pem'),
            InMemory::file('public.pem')
        );
    }

    // tokenの生成
    public function createToken(): string
    {
        $token = $this->config->builder()
            ->issuedBy('hogehoge')              // iss
            ->relatedTo('Hello!')               // sub
            ->withClaim('name', 'fugafuga')     // name(パブリッククレーム)
            ->withClaim('admin', true)          // admin(プライベートクレーム)
            ->issuedAt(new DateTimeImmutable()) // iat
            ->getToken($this->config->signer(), $this->config->signingKey());

        return $token->toString();
    }

    // tokenの検証
    public function checkToken(string $jwt): bool
    {
        $token = $this->config->parser()->parse($jwt);
        $this->config->setValidationConstraints(...[
            // 署名検証
            new SignedWith($this->config->signer(), $this->config->verificationKey()),
            // iss検証
            new IssuedBy('hogehoge'),                                       
            // sub検証
            new RelatedTo('Hello!')                                             
        ]);

        // バリデーションエラーを例外で返したい場合はassert()を使用する
        return $this->config->validator()->validate($token, ...$this->config->validationConstraints());
    }

    // tokenからデータを取得
    public function getData(string $jwt): array
    {
        $token = $this->config->parser()->parse($jwt);

        return $token->claims()->all();
    }
}

$lcobucci_jwt = new LcobucciJWT();
$jwt = $lcobucci_jwt->createToken();
$lcobucci_jwt->checkToken($jwt);
$lcobucci_jwt->getData($jwt);
```

# 感想
JWTはauth0の特設サイトやライブラリが充実しているため、手軽に検証ができます。
ライブラリはドキュメントの他にテストコードも実装の参考になりました！

# 採用PR
弊社で一緒に働く仲間を募集しています。
全てのオタクを幸せにしたい方、是非ご覧ください！

https://hrmos.co/pages/vivion/jobs/0000191