PKCEのCode_VerifierとCode_ChallengeをJMeterリクエストに組み込む方法

# はじめに
OAuthでの認可コード横取り攻撃対策としてPKCE([RFC 7636](https://tools.ietf.org/html/rfc7636))という仕様が策定され、クライアント側でCode_VerifierとCode_Challengeと呼ばれるコードを生成し、リクエストのパラメータに埋め込む仕様になっている。

PKCE対応したAPIサービス開発でJMeterを利用した負荷試験を実施することになり、クライアント側のコード生成処理をJMeterに組み込むことなったため、その時に組み込んだ方法や手順について解説する。

## Code_Verifier、Code_Challengeの生成
JMeterでリクエストに埋め込むコード(Code_Verifier、Code_Challenge)を生成するため、コード生成するクラスを含むjarを生成する。
コード生成アルゴリズムは[RFC 7636](https://tools.ietf.org/html/rfc7636#page-8)にて下記の通り定義されているので、仕様に従ってコード生成を行うJavaプログラムを実装する。

> 
   code_verifier = high-entropy cryptographic random STRING using the
   unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
   from Section 2.3 of [RFC3986], with a minimum length of 43 characters
   and a maximum length of 128 characters.

>  省略  

> 
  code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

>  省略  

>  ABNF for "code_challenge" is as follows.

>  code-challenge = 43*128unreserved
   unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
   ALPHA = %x41-5A / %x61-7A
   DIGIT = %x30-39

Code_Challengeの生成でSHA256Hash化アルゴリズムを利用するため、`commons-codec`を、Base64エンコーディングのために`commons-lang3`を利用する。

```java
package com.sample.pkce;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.Base64;

public class PKCECode {

    private final String CODE_VERFIER_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~";

    public String getCodeVerifier(final int codeVerifierLength) {
        return RandomStringUtils.random(codeVerifierLength, CODE_VERFIER_STRING);
    }

    public String getCodeChallenge(final String codeVerifier) {
        final byte[] hashedCodeVerifier = DigestUtils.sha256(codeVerifier);
        final String base64EncodedCodeVerifier = Base64.getEncoder().encodeToString(hashedCodeVerifier);
        final String base64URLEncodedCodeVerifier = base64EncodedCodeVerifier
                .replaceAll("=","")
                .replaceAll("\\+","-")
                .replaceAll("/","_");
        return base64URLEncodedCodeVerifier;
    }
}
```

実装したプログラムが正しくCode_Challengeを生成できているか確認のため、[RFC 7636のAppendix](https://tools.ietf.org/html/rfc7636#appendix-B)の変換例をと同じCodeVerifierを利用してテストを行い、問題なく変換できていることが確認できた。

```java
@Test
public void getCodeChallengeTest() {
    final String codeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
    final PKCECode pkceCode = new PKCECode();
    Assert.assertEquals("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", pkceCode.getCodeChallenge(codeVerifier));
}
```


## JMeterでリクエスト
JMeterを起動する前に、利用したライブラリの`commons-codec`、`commons-lang3`および生成されたjarファイルをJMeterの外部libフォルダ(`<JMeterインストール先>\lib\ext`）に格納する。

その後、JMeterを起動し、テスト計画に「追加」「前処理」から`BeanShell PreProcessor`を選択し下記スクリプトを入力する。

```java
import com.sample.pkce.PKCECode 

PKCECode pkceCode = new PKCECode();
String codeVerifier = pkceCode.getCodeVerifier(43); //Code_Verifierの文字長を指定する
String codeChallenge= pkceCode.getCodeChallenge(codeVerifier);

vars.put("codeVerifier",codeVerifier );
vars.put("codeChallenge",codeChallenge);
```

その後、HTTPリクエストのリクエストパラメータ等に`${codeVerifier}`、`${codeChallenge}`でスクリプトで埋め込んだ値をリクエスト設定します。

## 補足
今回は`BeanShell PreProcessor`でコードの生成処理を組み込む方法について解説したが、毎回コード生成を行うとクライアントに処理負荷がかかり、思うように負荷をかけることができない場合がある。そのため、性能負荷試験を行う場合は下記2通りのいずれかを選択することを推奨する。

* 事前に大量のコードをCSVファイルに出力して、JMeterの``で読み込ませる方法
* 全リクエストでCode_Verifier、Code_Challengeを固定値として、JMeterのパラメータに直接設定する
  * 固定値でよいのであれば、わざわざ新しくコード生成せずに[RFC 7636のAppendix](https://tools.ietf.org/html/rfc7636#appendix-B)の変換例を利用する方法もある

## 参考
なぜ、43文字~128文字なのかなどの解説についてはこちら詳しく解説していました。参考にどうぞ。

* https://techblog.yahoo.co.jp/entry/20191219790463/