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

# **PKCE とは**

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

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

この仕様自体のちょっとした解説は、「[OAuth & OpenID Connect 関連仕様まとめ](http://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3)」の「[7. 認可コード横取り攻撃への対抗策 (RFC 7636)](http://qiita.com/TakahikoKawasaki/items/185d34814eb9f7ac7ef3#7-%E8%AA%8D%E5%8F%AF%E3%82%B3%E3%83%BC%E3%83%89%E6%A8%AA%E5%8F%96%E3%82%8A%E6%94%BB%E6%92%83%E3%81%B8%E3%81%AE%E5%AF%BE%E6%8A%97%E7%AD%96-rfc-7636)」に書いてあります。また、英語ではありますが、懇切丁寧なイラスト付きで解説したものが [Authlete 社](https://www.authlete.com/)<sup>*</sup> のウェブサイト上にあります。

> **Proof Key for Code Exchange (RFC 7636)**
> https://www.authlete.com/developers/pkce/

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

<sup>* この記事の筆者は [Authlete 社](https://www.authlete.com/)の創業者です。</sup>


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

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

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

![pkce_authorization_response.png](https://qiita-image-store.s3.amazonaws.com/0/106044/3ea17ee2-bd22-c6e8-ccd6-f0e6e1462b7d.png)

もう一つは、[トークンエンドポイント](https://tools.ietf.org/html/rfc6749#section-3.2)において、トークンリクエストから `code_verifier` リクエストパラメーターの値を取り出し、その `code_verifier` とデータベースに保存しておいた `code_challenge_method` を用いて `code_challenge` を計算し、その計算した値がデータベースに保存しておいた `code_challenge` の値と等しいかどうかチェックすることです。

![pkce_token_response.png](https://qiita-image-store.s3.amazonaws.com/0/106044/f0a3b23a-27f3-9639-86c3-d2667841b89b.png)


# **Authlete の実装コード**

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

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

```java
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();

    // The authorization request did not contain 'code_challenge'.
    if (challenge == null)
    {
        // If the token request contains 'code_verifier' even if it does
        // not have to do it.
        if (extractFromParameters("code_verifier") != null)
        {
            // The token request contains 'code_verifier' although its
            // corresponding authorization request did not contain
            // 'code_challenge'.
            throw toException(invalid_grant, A050317);
        }

        // PKCE is not used.
        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);

    // If the format of the value of 'code_verifier' is invalid.
    if (CODE_VERIFIER_PATTERN.matcher(verifier).matches() == false)
    {
        // The value of 'code_verifier' in the token request does not
        // conform to the format defined in RFC 7636.
        throw toException(invalid_grant, A050316);
    }

    // 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)` メソッド内で使われている <code>[Digest](http://takahikokawasaki.github.io/nv-digest/com/neovisionaries/security/Digest.html)</code> クラスですが、これは、私がオープンソースで公開している [nv-digest](https://github.com/TakahikoKawasaki/nv-digest) というライブラリに含まれています。ダイジェスト計算を簡単に実行するためのライブラリです。このライブラリを使うと、SHA-256 ダイジェスト値を得るのも、ワンライナーで済みます。

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

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

こちらも二つです。

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

![pkce_authorization_request.png](https://qiita-image-store.s3.amazonaws.com/0/106044/6474d97b-a9cb-3d9e-ed15-8c7bcfb84eb1.png)

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

![pkce_token_request.png](https://qiita-image-store.s3.amazonaws.com/0/106044/9db23b24-68ef-c7d6-4ff2-c0dd4de85aa4.png)


# **クライアント側の実装例**

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

1. **[AppAuth for Android](http://openid.github.io/AppAuth-Android/)**
2. **[AppAuth for iOS and macOS](http://openid.github.io/AppAuth-iOS/)**

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

`code_challenge_method=S256` 時の `code_challenge` 計算ロジックを自作した場合は、その動作確認として、
`code_verifier` が `dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk` のときに `code_challenge` の値が `E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM` になるかどうかを確認するという手があります。これらの値は、[RFC 7636](https://tools.ietf.org/html/rfc7636) の「[Appendix B. Example for the S256 code_challenge_method](https://tools.ietf.org/html/rfc7636#appendix-B)」に例として挙げられているものです。


# **さいごに**

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

以上です！

追記：2019 年 7 月 8 日
コードチャレンジメソッド S256 を強制するための設定項目を Authlete 2.1 に追加しました。PKCE を常に要求する設定と組み合わせれば、常に `code_challenge_method=S256` を要求できるようになります。
![pkce_s256_required.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106044/13cbac28-a693-612e-8b3a-f308b6c16df2.png)