lcobucci/jwt 4系でZoom API用トークン作成 (Claimの日付系フォーマットを変える)

[[PHP] Zoom APIを利用してミーティングを作成する](https://qiita.com/kuzira_vimmer/items/d64f15447cb556eafd3b)で、Zoom APIを利用してミーティングを作成する方法が詳しく記載されていますが、[lcobucci/jwt](https://packagist.org/packages/lcobucci/jwt)の4系ではZoom API用のJWTトークンの作成方法が変わっています。
4系でのトークン作成について簡単に説明します。

## TL;DR
lcobucci/jwt4系は、デフォルトのClaimFormatterが日付系をマイクロ秒ありUnixタイムスタンプにフォーマットするので、Zoom APIが401を返してくる。[日付をマイクロ秒なしUNIXタイムスタンプにフォーマットしてトークン作成する](#%E6%97%A5%E4%BB%98%E3%82%92%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E7%A7%92%E3%81%AA%E3%81%97unix%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%B9%E3%82%BF%E3%83%B3%E3%83%97%E3%81%AB%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%81%97%E3%81%A6%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)ことで正常にZoom APIにアクセスできる。

## lobucci/jwtインストール
```
composer require lobucci/jwt
```

## トークン作成
### Configurationインスタンスを作る
3系ではBuilderを使ってトークン作成していましたが、4系ではまずConfigurationインスタンスを作成します。

```php

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;

$apiSecret = '[ZOOM API SECRET]';
$configuration = Configuration::forSymmetricSigner(
            new Sha256(), // 暗号化ハッシュ関数
            InMemory::plainText($apiSecret) // キー
        );
```

Zoomは、HMAC SHA256を利用しているので、ハッシュ関数には `Lcobucci\JWT\Signer\Hmac\Sha256`を、キーはZoom API管理画面から平文で取得できるので`Lcobucci\JWT\Signer\Key\InMemory::plainText()`でセットします。

### Builderからトークン作成、そして失敗
トークン作成用のBuilderは`$configuration->builder()`で取得できるので、これを利用します。

**※このコードでは動作しません！！！**

```php
$apiKey = '[ZOOM API KEY]';
$now = new \DateTimeImmutable();
$token = $configuration->builder()
    ->issuedBy($apiKey)
    ->issuedAt($now)
    ->expiresAt($now->modify('+1 hour'))
    ->getToken($configuration->signer(), $configuration->signingKey());
```

作成日時や有効期限といった日付系はUNIXタイムから`DateTimeImmutable`型に変更になっています。
Builderのそれぞれのメソッドに渡す値につぃては以下の通りです。

| メソッド名 | 引数 |
|:--|:--|
| issuedBy | APIキー |
| issuedAt | トークン作成日 |
| expiredAt | 有効期限 |

値をセットして、`getToken($configuration->signer(), $configuration->signingKey())`でトークンを作成しますが、**このトークンではZoom APIにアクセスしても401 Unauthorizedが返ってきます。**


## 作成したトークンでAPIにアクセスできない原因は日付系フォーマット
4系のBuilderで生成されたトークンのClaim部分とZoom APIの管理画面から生成した値を比べてみました。


```json:Zoom管理画面から生成したClaim部分
{
  "aud": null,
  "iss": "APIキー",
  "exp": 1610972172,
  "iat": 1610966772
}
```

```json:4系lcobucci/jwtで生成したClaim部分
{
  "iss": "APIキー",
  "iat": "1611019091.181495",
  "exp": "1611022691.181495"
}
```
4系では、日付系のフォーマットにマイクロ秒が付随しているのに対し、Zoom管理画面から生成したものにはマイクロ秒が付随していません。（audはなくても問題なし）

lcobucci/jwtのコードを追ってみると...

```php:ChainedFormatter.php

public static function default(): self
    {
        return new self(new UnifyAudience(), new MicrosecondBasedDateConversion()); // <- ここ
    }
```

```php:MicrosecondBasedDateConversion
    private function convertDate(DateTimeImmutable $date)
    {
        $seconds      = $date->format('U');
        $microseconds = $date->format('u');

        if ((int) $microseconds === 0) {
            return (int) $seconds;
        }

        return $seconds . '.' . $microseconds; // <- ここ
    }
```

というようになっています。lcobucci/jwtでは`Lcobucci\JWT\ClaimsFormatte`インターフェイスを実装したフォーマッタークラスがあり、これを利用してClaimをフォーマットしていますが、デフォルトでは日付はマイクロ秒つきのUnixタイムスタンプになるフォーマッターが使われてしまいます。これが原因でした。

## 日付をマイクロ秒なしUNIXタイムスタンプにフォーマットしてトークン作成する
### マイクロ秒なしUNIXタイムスタンプ用のフォーマッターを作る
まず、マイクロ秒なしにするためには独自のフォーマッターを作ります。

```php:UnixTimestampFormatter.php
<?php

use Lcobucci\JWT\ClaimsFormatter;
use Lcobucci\JWT\Token\RegisteredClaims;

class UnixTimestampFormatter implements ClaimsFormatter
{
    /** @inheritdoc  */
    public function formatClaims(array $claims): array
    {
        foreach (RegisteredClaims::DATE_CLAIMS as $claim) {
            if (! array_key_exists($claim, $claims)) {
                continue;
            }

            assert($claims[$claim] instanceof \DateTimeImmutable);
            $claims[$claim] = $claims[$claim]->getTimestamp();
        }

        return $claims;
    }
}
```
### 独自フォーマッターを使ってトークンを作成する
作成したフォーマッターは`$configuration->builder()`に引数で渡せます。独自フォーマッターを渡せば、あとは前述の通りでZoom APIにアクセスすることができるトークンを作成できます。Configurationインスタンス作成を含めた、一連のコードは以下の通りです。

```php

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use UnixTimestampFormatter;

$apiKey = '[ZOOM API KEY]';
$apiSecret = '[ZOOM API SECRET]';
$now = new \DateTimeImmutable();

// Configurationインスタンス作成
$configuration = Configuration::forSymmetricSigner(
            new Sha256(), // 暗号化ハッシュ関数
            InMemory::plainText($apiSecret) // キー
        );

// トークン作成
$token = $configuration->builder(new UnixTimestampFormatter())
    ->issuedBy($apiKey)
    ->issuedAt($now)
    ->expiresAt($now->modify('+1 hour'))
    ->getToken($configuration->signer(), $configuration->signingKey());

```

トークン作成後は、[[PHP] Zoom APIを利用してミーティングを作成する](https://qiita.com/kuzira_vimmer/items/d64f15447cb556eafd3b)と同様です。