PKCE実装で使うcode_verifierとcode_challengeをPHPで実装する。

こちらは[エキサイトホールディングス Advent Calendar 2021](https://qiita.com/advent-calendar/2021/excite-hd)の13日目の記事です。今回はPKCE実装で使用するcode_verifierとcode_challengeをPHPを使って実装してみたいと思います。

## PKCE（ピクシー）とは？
PKCE（Proof Key for Code Exchange by OAuth Public Clients）とは[RFC7636](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)で定義されている、認可コード横取り攻撃の対策として提案された仕様です。 認可コード横取り攻撃とは、認可コードを不正に取得することによって、アクセストークンなどのトークンを不正に取得する攻撃です。

PKCEに関しては、下記2つの記事がわかりやすかったです。
[OAuth2.0拡張仕様のPKCE実装紹介 〜 Yahoo! ID連携に導入しました - Yahoo! JAPAN Tech Blog](https://techblog.yahoo.co.jp/entry/20191219790463/)
[LINEログインをPKCE対応する | LINE Developers](https://developers.line.biz/ja/docs/line-login/integrate-pkce/)

今回は、PKCEに必要なcode_verifierとcode_challengeをPHPを使って実装します。

## code_verifierの生成

code_verifierの仕様は、[RFC7636](https://datatracker.ietf.org/doc/html/rfc7636) に準拠して作成します。

>   NOTE: The code verifier SHOULD have enough entropy to make it
   impractical to guess the value.  It is RECOMMENDED that the output of
   a suitable random number generator be used to create a 32-octet
   sequence.  The octet sequence is then base64url-encoded to produce a
   43-octet URL safe string to use as the code verifier.


- 推測困難な乱数値を使うために、適当な乱数生成器を用いる。
- 十分なエントロピー(256ビット以上)を持たせるために32オクテットの値を利用。
- 構成文字： [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
- 文字数：43文字〜128文字

上記仕様を満たすように実装をしていきます。

### 乱数ジェネレーターを使用して32オクテットの値を生成

十分なエントロピーを持っている値を生成します。
適切な乱数ジェネレーターを使用して32オクテットの値を生成します。（1オクテット = 8ビット）

PHPの `openssl_random_pseudo_bytes();` は、疑似ランダムなバイト文字列を生成します。

```php
openssl_random_pseudo_bytes(32)
=> string(32) "Gs�<NKD�3[mm�X�t��=7Hm�"!�k�H"
```

ここで、「1バイトが必ずしも8ビットとは限らないのでは？」と思った方もいるかもしれません。

`openssl_random_pseudo_bytes();` はC言語で実装されています。
C言語では、`<limits.h>`ヘッダで定義されるCHAR_BITマクロでchar型のビット数を定義しています。
CHAR_BITマクロは少なくとも1バイトが8ビット以上に定義されることが保証されているので、1バイト文字列が8ビット以上であると考えて良さそうです。

### Base64でエンコードを行う。

base64は、A-Z（26）、a-z（26）、0-9（10）、+, /（2）、パティングを表す=を使って表現されます。

```php
base64_encode(openssl_random_pseudo_bytes(32)
=> string(44) "euG+/gkfxtLrZakr5GqZojfSBB4t9lLKogDNW4/gauM="
```

### 使用可能な構成文字に対応をする。
URL safeにする必要があるので、+, /を置き換えて、=を削除します。


```php
str_replace('=', '', strtr(base64_encode(openssl_random_pseudo_bytes(32)), '+/', '-_'));
=> string(43) "jIhzUc6JtFe1OQSg4v6-VbJuqhVZBalkq0FEEzwYoxs"
```

### code_verifierを生成する関数

code_verifierを生成する関数の完成です。

```php
private function generateCodeVerifier(int $byteLength = 32){
    return str_replace('=', '', strtr(base64_encode(openssl_random_pseudo_bytes($byte_length)), '+/', '-_'));
}
```

読みやすさを意識して、説明変数を使うと下記のようになります。

```php
private function generateCodeVerifier(int $byteLength = 32)
{
    $randomBytesString = openssl_random_pseudo_bytes($byteLength);
    $encodedRandomString = base64_encode($randomBytesString);
    $urlSafeEncoding = [
        '=' => '',
        '+' => '-',
        '/' => '_',
    ];
    return strtr($encodedRandomString, $urlSafeEncoding);
}   
```


## code_challengeの生成

code_verifierをSHA256で暗号化し、Base64URL形式にエンコードすることでcode_challengeを生成できます。
また、「=」の削除、+を-に、/を_に置換します。

### code_challengeを生成する関数

```php
    private function generateCodeChallenge($code_verifier)
    {
        $hash = hash('sha256', $code_verifier, true);
        return str_replace('=', '', strtr(base64_encode($hash), '+/', '-_'));
    }
```

## まとめ

PKCE実装のPHPでの実装例があまりなかったので、作成してみました。
より良い書き方や改善点があればコメントいただけると幸いです。


弊社のアドベントカレンダー他記事もよろしくお願いしますー！
https://qiita.com/advent-calendar/2021/excite-hd