はじめに
日立製作所の@tnorimatと申します。面白い企画に相乗りさせて頂きます。Keycloakのパッチ開発の注意点と、開発したパッチであるRFC7636対応について紹介します。
まず、今回対象となるRFC7636(通称PKCE:ぴくしー)は、OAuthの認可コード横取り攻撃対策のための規格です。PKCEそのものについての説明は、他によい記事がありますので、ここでは割愛します。例えば、下記の記事からたどれる一連の記事を参考にするとよいでしょう。
@TakahikoKawasaki著 PKCE: 認可コード横取り攻撃対策のために OAuth サーバーとクライアントが実装すべきこと
KeycloakへのPKCE対応パッチ開発について
KeycloakがPKCEに対応していないことに気づき、筆者が認可サーバーにおける実装パッチを提案し、バージョン3.1.0でマージされました。
パッチのマージにあたり、初めてだったので色々と苦労しましたので、気づいたことを記します。今後Keycloakへパッチ開発される方は参考にして頂けると幸いです。
- developerメーリングリストは埋もれる
Keycloakのオフィシャルページには、パッチ開発にあたっては、まずはdeveloperメーリングリストで議論するようにありましたが、容易に埋もれます。
- JIRAとPull Requestが優先
Keycloakでは、課題管理にはJBossのJIRA、ソース管理にはGithubが使われています。これらを通じて議論するのが埋もれずに済むコツです。作りたい機能がある場合は、まずは、これらを検索し、関連トピックがあればそちらで議論、なければ作成するとよいでしょう。
- テストを書く
パッチを作ったら、Githubのリポジトリにpull requestを出すことで提出するのですが、この際、テストを書く必要があることに注意してください。
パッチの内容によっては、テストコードはArquillianという統合テスト用フレームワークを使う必要があり、慣れないうちは骨が折れます。テストが必要なことから、あまり大きな機能にしてしまうと、レビューで根本的な指摘があると大変です。そのため、なるべく小さな機能に区切ってパッチを出すとよいでしょう。
Arquillianは、Javaベースのプログラムの統合テスト用フレームワークです。初めて使用される方は、以下のサイトを参照することをお勧めします。
Arquillian Guides - Getting Started
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)、そしてソースコードを入手します。
認可サーバー本体は、以下のサイトから入手できます。
Wildfly用クライアントアダプター(OpenID Connect)は以下のサイトから入手できます。
ソースコードは、以下のサイトから入手できます。
Keycloak 3.4.0.Finalのインストール
認可サーバーのインストール方法は、以下のサイトに記載されています。
今回は、認可サーバーと同じAPサーバー(wildfly)に、クライアントアプリケーションをdeployします。wildfly用クライアントアダプター(OpenID Connect)のインストール方法は、以下のサイトに記載されています。
デモ用の環境の設定
今回の試行のために、Keycloakに添付されている、demo-templateというデモ用環境を設定します。その方法は、以下のサイトに記載されています。
PKCE対応しているクライアントアプリケーションの準備
前述の通り、前述のパッチは認可サーバー側の実装です。クライアント側については、クライアントアダプターがPKCE未対応であるため、記事執筆時点では自作が必要です。今回は、筆者がJavaサーブレット用の実装としてKeycloakコミュニティに提案中の下記パッチを適用することで、PKCE対応クライアントWebアプリを作成します。
お手持ちのKeycloakのソースコードに、上記サイトのパッチをマージし、mvn clean install
でビルドします。
ビルドで生成されたwarファイル(/examples/demo-template/third-party/target/oauth-client.war)を、APサーバーであるWildflyにdeployして下さい。deployする方法は、以下のサイトに記載されています。
結果
-
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
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
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の仕様にあるとおり、認可サーバーから、クライアントへエラー応答が返って来ました。
おまけ
- PKCEを実装していないクライアントの扱い
現時点(3.4.0.Final)では、KeycloakはPKCEのプロトコルに準拠していないクライアントが認可コード要求を出してきた場合、つまり、code challengeとcode challenge methodパラメータを付与せず認可コード要求を出してきた場合、通常通り認可コードフローを実行します。
- PKCEのテストケース
KeycloakのPKCE機能について、Arquillianでの統合テスト用のテストケースのクラス(org.keycloak.testsuite.oauth.OAuthProofKeyForCodeExchangeTest)があります。
これを実行(テスト)し、その際出力されるログや、キャプチャされたパケットから、PKCEのプロトコル処理の様子を見ることができます。