はじめに
OAuthでの認可コード横取り攻撃対策としてPKCE(RFC 7636)という仕様が策定され、クライアント側でCode_VerifierとCode_Challengeと呼ばれるコードを生成し、リクエストのパラメータに埋め込む仕様になっている。
PKCE対応したAPIサービス開発でJMeterを利用した負荷試験を実施することになり、クライアント側のコード生成処理をJMeterに組み込むことなったため、その時に組み込んだ方法や手順について解説する。
Code_Verifier、Code_Challengeの生成
JMeterでリクエストに埋め込むコード(Code_Verifier、Code_Challenge)を生成するため、コード生成するクラスを含むjarを生成する。
コード生成アルゴリズムはRFC 7636にて下記の通り定義されているので、仕様に従ってコード生成を行う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
を利用する。
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の変換例をと同じCodeVerifierを利用してテストを行い、問題なく変換できていることが確認できた。
@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
を選択し下記スクリプトを入力する。
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の変換例を利用する方法もある
参考
なぜ、43文字~128文字なのかなどの解説についてはこちら詳しく解説していました。参考にどうぞ。