1. Qiita
  2. 投稿
  3. OAuth

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

  • 14
    いいね
  • 0
    コメント

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/documents/article/pkce/index

ですので、ここでは、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

これらは、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 を伴わない認可リクエストを全て拒否する」という動作を設定で選択することも可能です。

以上です!

Comments Loading...