Edited at

PKCE: 認可コード横取り攻撃対策のために OAuth サーバーとクライアントが実装すべきこと


PKCE とは

PKCE をご存知でしょうか? これは、今から一年ほど前の 2015 年 9 月に RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) として公開された仕様を指しています。認可コード横取り攻撃 (authorization code interception attack) への対策として策定されました。

細かい条件は幾つかありますが、スマートフォンで OAuth クライアントを作る場合は、クライアント側も認可サーバー側もこの仕様の実装が強く推奨されます。これを実装しておかないと、悪意のあるアプリケーションに認可コードを横取りされてしまい、結果、悪意のあるアプリケーションがアクセストークンを取得できてしまいます。

この仕様自体のちょっとした解説は、「OAuth & OpenID Connect 関連仕様まとめ」の「7. 認可コード横取り攻撃への対抗策 (RFC 7636)」に書いてあります。また、英語ではありますが、懇切丁寧なイラスト付きで解説したものが Authlete 社* のウェブサイト上にあります。


Proof Key for Code Exchange (RFC 7636)

https://www.authlete.com/developers/pkce/


ですので、ここでは、PKCE の仕様自体の解説は省略します。

* この記事の筆者は Authlete 社の創業者です。


サーバー側実装でやること

簡単に言うと、二つです。

一つは、認可エンドポイントにおいて、認可コードフローによるリクエストを処理するとき、生成した認可コードと一緒に、認可リクエストに含まれていた code_challenge リクエストパラメーターの値と code_challenge_method リクエストパラメーターの値をデータベースに保存しておくことです。次の図は Authlete 社のウェブサイトからの転載です (以降同様です)。

pkce_authorization_response.png

もう一つは、トークンエンドポイントにおいて、トークンリクエストから code_verifier リクエストパラメーターの値を取り出し、その code_verifier とデータベースに保存しておいた code_challenge_method を用いて code_challenge を計算し、その計算した値がデータベースに保存しておいた code_challenge の値と等しいかどうかチェックすることです。

pkce_token_response.png


Authlete の実装コード

認可エンドポイントでの処理は、単に code_challengecode_challenge_method の値をデータベースに保存するだけの処理なので、特にコードを見ても面白いことはありません。知見としては、「PKCE をサポートする認可サーバーは、認可コードを管理するデータベーステーブルに、code_challengecode_challenge_method を保存するカラムを追加する必要がある」、ということくらいです。

というわけで、Authlete 全体のコードは企業秘密ですが、トークンエンドポイントにおける code_verifier の検証部分だけ公開します! (まぁ、たいしたことはやっていないです)

private void validatePKCE(AuthorizationCodeEntity acEntity)

{
// See RFC 7636 (Proof Key for Code Exchange) for details.

// Get the value of 'code_challenge' which was contained in
// the authorization request.
String challenge = acEntity.getCodeChallenge();

if (challenge == null)
{
// The authorization request did not contain 'code_challenge'.
return;
}

// If the authorization request contained 'code_challenge', the
// token request must contain 'code_verifier'. Extract the value
// of 'code_verifier' from the token request.
String verifier = extractFromParameters(
"code_verifier", invalid_grant, A050312, A050313, A050314);

// Compute the challenge using the verifier
String computedChallenge = computeChallenge(acEntity, verifier);

if (challenge.equals(computedChallenge))
{
// OK. The presented code_verifier is valid.
return;
}

// The code challenge value computed with 'code_verifier' is different
// from 'code_challenge' contained in the authorization request.
throw toException(invalid_grant, A050315);
}

private String computeChallenge(AuthorizationCodeEntity acEntity, String verifier)
{
CodeChallengeMethod method = acEntity.getCodeChallengeMethod();

// This should not happen, but just in case.
if (method == null)
{
// Use 'plain' as the default value required by RFC 7636.
method = CodeChallengeMethod.PLAIN;
}

switch (method)
{
case PLAIN:
// code_verifier
return verifier;

case S256:
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
return computeChallengeS256(verifier);

default:
// The value of code_challenge_method extracted from the database is not supported.
throw toException(server_error, A050102);
}
}

private String computeChallengeS256(String verifier)
{
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

// SHA256
byte[] hash = Digest.getInstanceSHA256().update(verifier).digest();

// BASE64URL
return SecurityUtils.encode(hash);
}

なお、computeChallengeS256(String) メソッド内で使われている Digest クラスですが、これは、私がオープンソースで公開している nv-digest というライブラリに含まれています。ダイジェスト計算を簡単に実行するためのライブラリです。このライブラリを使うと、SHA-256 ダイジェスト値を得るのも、ワンライナーで済みます。

byte[] hash = Digest.getInstanceSHA256().update(verifier).digest();


クライアント側実装でやること

こちらも二つです。

一つは、認可リクエストを投げる際、43 ~ 128 文字の code_verifier を生成して、使用する code_challenge_method のロジック (plain もしくは S256) で code_challenge を計算し、code_challenge_methodcode_challenge をリクエストに含めることです。

pkce_authorization_request.png

もう一つは、トークンリクエストを投げる際、code_verifier をリクエストに含めることです。

pkce_token_request.png


クライアント側の実装例

code_verifier から code_challenge を計算するロジックは、上記に挙げた Authlete の実装コードと同じなのですが、ここでは特に次の二つを紹介しようと思います。


  1. AppAuth for Android

  2. AppAuth for iOS and macOS

これらは、OAuth 2.0 & OpenID Connect サーバーとやりとりするためのクライアント SDK であり、諸々のベストプラクティスを含んでいます。PKCE もサポートされているとのことなので、参考になると思います。

code_challenge_method=S256 時の code_challenge 計算ロジックを自作した場合は、その動作確認として、

code_verifierdBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk のときに code_challenge の値が E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM になるかどうかを確認するという手があります。これらの値は、RFC 7636 の「Appendix B. Example for the S256 code_challenge_method」に例として挙げられているものです。


さいごに

認可サーバーを選ぶときは、PKCE をサポートしているものを選びましょう。Authlete は PKCE をサポートしていますが、さらに、「code_challenge を伴わない認可リクエストを全て拒否する」という動作を設定で選択することも可能です。

以上です!

追記:2019 年 7 月 8 日

コードチャレンジメソッド S256 を強制するための設定項目を Authlete 2.1 に追加しました。PKCE を常に要求する設定と組み合わせれば、常に code_challenge_method=S256 を要求できるようになります。

pkce_s256_required.png