Java
IAM
PKCE
Keycloak
RFC7636

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

はじめに

日立製作所の@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)、そしてソースコードを入手します。

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

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 Generated codeVerifier = zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg

2017-11-20 10:29:17,356 DEBUG org.keycloak.servlet.ServletOAuthClient 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_challengecode_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 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 PKCE supporting Client, codeVerifier = zns5R5Xmak7CXzifpPKjZalyYn-gBfbYGyqgZbSgheg

2017-11-20 10:29:24,097 DEBUG org.keycloak.protocol.oidc.endpoints.TokenEndpoint PKCE codeChallengeMethod = S256

2017-11-20 10:29:24,097 DEBUG org.keycloak.protocol.oidc.endpoints.TokenEndpoint PKCE verification success. codeVerifierEncoded = IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE, codeChallenge = IWLIhv1NvG0tOvJe22Ke0c1Am02rUKS0LMTS0pgY3XE

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

  • APサーバーのログ

2017-11-20 10:29:24,119 DEBUG org.keycloak.events 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 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 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 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のプロトコル処理の様子を見ることができます。