RFC7636（PKCE:Proof Key for Code Exchange by OAuth Public Clients）対応を試してみる

# はじめに

日立製作所の@tnorimatと申します。面白い企画に相乗りさせて頂きます。Keycloakのパッチ開発の注意点と、開発したパッチであるRFC7636対応について紹介します。

まず、今回対象となるRFC7636（通称PKCE：ぴくしー）は、OAuthの認可コード横取り攻撃対策のための規格です。PKCEそのものについての説明は、他によい記事がありますので、ここでは割愛します。例えば、下記の記事からたどれる一連の記事を参考にするとよいでしょう。

[@TakahikoKawasaki著　PKCE: 認可コード横取り攻撃対策のために OAuth サーバーとクライアントが実装すべきこと](https://qiita.com/TakahikoKawasaki/items/00f333c72ed96c4da659)


# KeycloakへのPKCE対応パッチ開発について

KeycloakがPKCEに対応していないことに気づき、筆者が認可サーバーにおける実装[パッチ](https://github.com/keycloak/keycloak/pull/3831)を提案し、バージョン3.1.0でマージされました。

パッチのマージにあたり、初めてだったので色々と苦労しましたので、気づいたことを記します。今後Keycloakへパッチ開発される方は参考にして頂けると幸いです。

- developerメーリングリストは埋もれる

[Keycloakのオフィシャルページ](http://www.keycloak.org/community.html)には、パッチ開発にあたっては、まずは[developerメーリングリスト](https://lists.jboss.org/mailman/listinfo/keycloak-dev)で議論するようにありましたが、容易に埋もれます。

- JIRAとPull Requestが優先

Keycloakでは、課題管理にはJBossの[JIRA](https://issues.jboss.org/secure/Dashboard.jspa)、ソース管理には[Github](https://github.com/keycloak/)が使われています。これらを通じて議論するのが埋もれずに済むコツです。作りたい機能がある場合は、まずは、これらを検索し、関連トピックがあればそちらで議論、なければ作成するとよいでしょう。

- テストを書く

パッチを作ったら、Githubのリポジトリにpull requestを出すことで提出するのですが、この際、テストを書く必要があることに注意してください。

パッチの内容によっては、テストコードはArquillianという統合テスト用フレームワークを使う必要があり、慣れないうちは骨が折れます。テストが必要なことから、あまり大きな機能にしてしまうと、レビューで根本的な指摘があると大変です。そのため、なるべく小さな機能に区切ってパッチを出すとよいでしょう。

Arquillianは、Javaベースのプログラムの統合テスト用フレームワークです。初めて使用される方は、以下のサイトを参照することをお勧めします。

[Arquillian Guides - Getting Started](http://arquillian.org/guides/getting_started_ja/)


# PKCEの動作確認してみよう

さて、ここからは、今回開発した機能であるPKCE対応の動作を確認してみます。Keycloakに同梱のサンプルプログラムをクライアントとして使用し、APサーバーのログとパケットキャプチャで、RFC 7636がどのように動いているかを確認します。

## 詳細

使用するソフトウェアコンポーネントを以下に示します。

- Keycloakのバージョン： 3.4.0.Final（2017/11/20時点での最新バージョン）

- クライアント： oauth-client-exampleプロジェクト

前述のパッチは認可サーバー側の実装であるため、今回はクライアント側の処理を上述のプロジェクトのアプリケーションに実装し、使用しています。

- リソースサーバー: database-serviceプロジェクト

クライアント、リソースサーバー双方とも、認可サーバーであるKeycloakと同じAPサーバー上にdeployします。

### Keycloak 3.4.0.Finalの入手

Keycloak 3.4.0.Finalの認可サーバー本体、wildfly用クライアントアダプター（OpenID Connect）、そしてソースコードを入手します。

認可サーバー本体は、以下のサイトから入手できます。

https://downloads.jboss.org/keycloak/3.4.0.Final/keycloak-3.4.0.Final.zip

Wildfly用クライアントアダプター（OpenID Connect）は以下のサイトから入手できます。

https://downloads.jboss.org/keycloak/3.4.0.Final/adapters/keycloak-oidc/keycloak-wildfly-adapter-dist-3.4.0.Final.zip

ソースコードは、以下のサイトから入手できます。

https://github.com/keycloak/keycloak/archive/3.4.0.Final.zip

### Keycloak 3.4.0.Finalのインストール

認可サーバーのインストール方法は、以下のサイトに記載されています。

http://www.keycloak.org/docs/latest/server_installation/index.html#installation

今回は、認可サーバーと同じAPサーバー（wildfly）に、クライアントアプリケーションをdeployします。wildfly用クライアントアダプター（OpenID Connect）のインストール方法は、以下のサイトに記載されています。

http://www.keycloak.org/docs/latest/securing_apps/index.html#_jboss_adapter

### デモ用の環境の設定

今回の試行のために、Keycloakに添付されている、demo-templateというデモ用環境を設定します。その方法は、以下のサイトに記載されています。

https://github.com/keycloak/keycloak/tree/3.4.0.Final/examples/demo-template

### PKCE対応しているクライアントアプリケーションの準備

前述の通り、前述のパッチは認可サーバー側の実装です。クライアント側については、クライアントアダプターがPKCE未対応であるため、記事執筆時点では自作が必要です。今回は、筆者がJavaサーブレット用の実装としてKeycloakコミュニティに提案中の下記パッチを適用することで、PKCE対応クライアントWebアプリを作成します。

https://github.com/keycloak/keycloak/pull/4719/commits/20fdb537f50abe36d5a083566afd34038b050229


お手持ちのKeycloakのソースコードに、上記サイトのパッチをマージし、`mvn clean install` でビルドします。

ビルドで生成されたwarファイル（/examples/demo-template/third-party/target/oauth-client.war）を、APサーバーであるWildflyにdeployして下さい。deployする方法は、以下のサイトに記載されています。

https://docs.jboss.org/author/display/WFLY10/Admin+Guide#AdminGuide-Applicationdeployment

## 結果

- APサーバー（wildfly）のログと、キャプチャしたパケットの内容から、RFC 7636 PKCEのプロトコル処理がどのように行われているかを示します。

- ここでは、PKCEのプロトコル処理が成功している例を示します。

- 認可サーバー（Keycloak）、クライアントアプリケーション、リソースサーバーは、全て同じAPサーバー上にdeployされています。その為これら3者のログは、全て同じログファイルに出力されています。

### クライアントアプリがAuthorization Code Flowを開始

- APサーバーのログ

>2017-11-20 10:29:17,356 DEBUG [org.keycloak.servlet.ServletOAuthClient] (default task-1) Generated codeVerifier = zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg

>2017-11-20 10:29:17,356 DEBUG [org.keycloak.servlet.ServletOAuthClient] (default task-1) **Encode codeChallenge = IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE, codeChallengeMethod = S256**

- キャプチャしたパケット

>GET /auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=third-party&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth-client%2Fpull_data.jsp&state=0%2Fa437c3b3-b696-46c3-8c0f-ce8aae719616&scope=openid&**code_challenge=IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE**&**code_challenge_method=S256** HTTP/1.1

RFC 7636の仕様通りに、パラメータ（**code_challenge**、**code_challenge_method**）が指定されています。

### エンドユーザーが、認可サーバーにてログイン

- キャプチャしたパケット

>HTTP/1.1 302 Found

>Location: http://localhost:8080/oauth-client/pull_data.jsp?state=0%2Fa437c3b3-b696-46c3-8c0f-ce8aae719616&code=uss.-FniBUXUy4f4X8YixU2B6bqV1nuwz407h6O9-5Qj3yc.dc289513-eaf6-48b7-8fd8-6539f54cc29b.939f0088-18ad-4a47-912e-b4fcf7076825

RFC 6749の仕様通り、認可コード（code）が払い出されています。認可サーバーがエンドユーザーのブラウザをリダイレクトさせることで、クライアントにこの認可コードを渡します。

### クライアントアプリが、認可サーバーへ、トークン取得要求

- APサーバーのログ

>2017-11-20 10:29:24,022 DEBUG [org.keycloak.servlet.ServletOAuthClient] (default task-4) Before sending Token Request, codeVerifier = zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg

- キャプチャしたパケット

> POST /auth/realms/demo/protocol/openid-connect/token HTTP/1.1 

>grant_type=authorization_code&code=uss.-FniBUXUy4f4X8YixU2B6bqV1nuwz407h6O9-5Qj3yc.dc289513-eaf6-48b7-8fd8-6539f54cc29b.939f0088-18ad-4a47-912e-b4fcf7076825&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth-client%2Fpull_data.jsp&**code_verifier=zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg**

RFC 7636の仕様通り、クライアントはパラメータ（code_verifier）を送信しています。

### 認可サーバーが、トークン要求の検証を実施

- APサーバーのログ

>2017-11-20 10:29:24,096 DEBUG [org.keycloak.protocol.oidc.endpoints.TokenEndpoint] (default task-5) **PKCE supporting Client, codeVerifier = zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg**

>2017-11-20 10:29:24,097 DEBUG [org.keycloak.protocol.oidc.endpoints.TokenEndpoint] (default task-5) **PKCE codeChallengeMethod = S256**

>2017-11-20 10:29:24,097 DEBUG [org.keycloak.protocol.oidc.endpoints.TokenEndpoint] (default task-5) **PKCE verification success. codeVerifierEncoded = IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE, codeChallenge = IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE**

### 認可サーバーが、クライアントアプリへ、トークンを払出

- APサーバーのログ

>2017-11-20 10:29:24,119 DEBUG [org.keycloak.events] (default task-5) type=CODE_TO_TOKEN, realmId=448061c7-47ec-48f1-a7f2-1220825fff8f, clientId=third-party, userId=b2d04404-83b4-40ed-bc4d-0918c3a281fe, ipAddress=127.0.0.1, token_id=217e68ea-749d-4443-a995-52efa6a84bc3, grant_type=authorization_code, refresh_token_type=Refresh, refresh_token_id=2aa2783e-82cc-46e2-b6b2-ae3442371d31, code_id=dc289513-eaf6-48b7-8fd8-6539f54cc29b, client_auth_method=client-secret

- キャプチャしたパケット

>HTTP/1.1 200 OK

>{"access_token":長いため割愛}

認可サーバー(Keycloak)から、クライアントに各種トークンが払い出されました。


次に、クライアントをちょっと改造し、わざとCode Verifierをトークン要求時に送信しないようにした場合の結果を示します。


### クライアントアプリがAuthorization Code Flowを開始

- キャプチャしたパケット

>GET /auth/realms/demo/protocol/openid-connect/auth?response_type=code&client_id=third-party&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth-client%2Fpull_data.jsp&state=0%2F1603116b-bf49-40ae-9913-fdaaa3cf9c58&scope=openid&**code_challenge=Hgl_oUpGOBqRXphp2-4gh-OTbpgJOnWn465e1I1k4zw**&**code_challenge_method=S256** HTTP/1.1

RFC 7636の仕様通りに、パラメータ（**code_challenge**, **code_challenge_method**）が指定されています。

### エンドユーザーが、認可サーバーにてログイン

- キャプチャしたパケット

>HTTP/1.1 302 Found

>Location: http://localhost:8080/oauth-client/pull_data.jsp?state=0%2F1603116b-bf49-40ae-9913-fdaaa3cf9c58&code=uss.8_NpulL3CLouKCoQf4588IXLjWzhZTOavZHXgayAngE.a53e4e6b-06de-4df5-b302-719e7e99d604.939f0088-18ad-4a47-912e-b4fcf7076825

RFC 6749の仕様通り、認可コード（code）が払い出されています。認可サーバーはエンドユーザーのブラウザをリダイレクトさせることで、クライアントにこの認可コードを渡します。

### クライアントアプリが、認可サーバーへ、トークン取得要求

- APサーバーのログ
>2017-11-20 11:22:28,689 DEBUG [org.keycloak.servlet.ServletOAuthClient] (default task-48) Before sending Token Request, drop codeVerifier intentionally = gy-Ho6M8UyVw98mNpASzpQ1XeRP51A07iIgb3eDfejU

- キャプチャしたパケット

>POST /auth/realms/demo/protocol/openid-connect/token HTTP/1.1 grant_type=authorization_code&code=uss.8_NpulL3CLouKCoQf4588IXLjWzhZTOavZHXgayAngE.a53e4e6b-06de-4df5-b302-719e7e99d604.939f0088-18ad-4a47-912e-b4fcf7076825&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth-client%2Fpull_data.jsp

RFC 7636の仕様に反して、わざとパラメータ（**code_verifier**）を送信していません。

### 認可サーバーが、トークン要求の検証を実施

- APサーバーのログ

>2017-11-20 11:22:28,758 WARN  [org.keycloak.protocol.oidc.endpoints.TokenEndpoint] (default task-49) **PKCE code verifier not specified**, authUserId = b2d04404-83b4-40ed-bc4d-0918c3a281fe, authUsername = stian

検証で失敗しています。認可コード要求時に、PKCEのプロトコル処理のCode Challengeを送ったのち、トークン要求時、Code Verifierを送っていないために、エラーとなりました。

### 認可サーバーが、クライアントアプリへ、エラー応答を返却

- APサーバーのログ

>2017-11-20 11:22:28,760 WARN  [org.keycloak.events] (default task-49) type=CODE_TO_TOKEN_ERROR, realmId=448061c7-47ec-48f1-a7f2-1220825fff8f, clientId=third-party, userId=b2d04404-83b4-40ed-bc4d-0918c3a281fe, ipAddress=127.0.0.1, error=code_verifier_missing, grant_type=authorization_code, code_id=a53e4e6b-06de-4df5-b302-719e7e99d604, client_auth_method=client-secret

- キャプチャしたパケット

>HTTP/1.1 400 Bad Request

>{"error":"invalid_grant","error_description":"PKCE code verifier not specified"}

[RFC 7636の仕様](https://tools.ietf.org/html/rfc7636#section-4.4.1)にあるとおり、認可サーバーから、クライアントへエラー応答が返って来ました。

# おまけ

- PKCEを実装していないクライアントの扱い

現時点(3.4.0.Final)では、KeycloakはPKCEのプロトコルに準拠していないクライアントが認可コード要求を出してきた場合、つまり、code challengeとcode challenge methodパラメータを付与せず認可コード要求を出してきた場合、通常通り認可コードフローを実行します。

- PKCEのテストケース

KeycloakのPKCE機能について、Arquillianでの統合テスト用のテストケースのクラス(org.keycloak.testsuite.oauth.OAuthProofKeyForCodeExchangeTest)があります。

これを実行(テスト)し、その際出力されるログや、キャプチャされたパケットから、PKCEのプロトコル処理の様子を見ることができます。