Keycloakを用いたハードニングの実装方法
第四回は、第三回で紹介したハードニング方法を、実際にKeycloakを用いて実装し、動作を確認していきます。使用するシステムは、第二回で構築したもの(図1)を用います。
ハードニング①:TLS
まずは、通信をTLSで暗号化する方法です(図2)。通信をTLSで暗号化することで、盗聴が格段に難しくなります。
実際に設定していきます。通信をTLSで暗号化するために、ここではプライベート認証局を設けてサーバ証明書に署名を入れます。本番環境では信頼のおけるパブリック認証局に署名をもらってください。まずは、任意のホストをプライベート認証局として準備します。keytoolコマンドを用いて作成した証明書発行要求に署名できるように、opensslの設定ファイル(/etc/pki/tls/openssl.cnf)を変更します。
#string_mask = utf8only string_mask = pkix
opensslコマンドを用いてプライベート認証局の秘密鍵とCA証明書を作成します。
$ sudo openssl genrsa -out ca.key 2048 $ sudo openssl req -new -key ca.key -out ca.csr ... Country Name (2 letter code) [XX]:JP State or Province Name (full name) []:Kanagawa Locality Name (eg, city) [Default City]: Organization Name (eg, company) [Default Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:<認証局hostname> ... $ sudo openssl x509 -days 3650 -in ca.csr -req -signkey ca.key -out ca.crt
署名に必要なファイルを準備します。
$ sudo touch /etc/pki/CA/index.txt $ echo '00' | sudo tee /etc/pki/CA/serial
以上で、プライベート認証局の完成です。
次に、Keycloakとの通信をTLSで暗号化してみましょう。認可サーバで、keytoolコマンドを用いてキーストアおよび証明書発行要求を作成します。
$ cd keycloak-9.0.3/standalone/configuration/ $ keytool -genkey -alias <認可サーバhostname> -keyalg RSA -keystore keycloak.jks -validity 10950 キーストアのパスワードを入力してください: secret 新規パスワードを再入力してください: secret 姓名は何ですか。 [Unknown]: <認可サーバhostname> 組織単位名は何ですか。 [Unknown]: 組織名は何ですか。 [Unknown]: Default Company Ltd 都市名または地域名は何ですか。 [Unknown]: 都道府県名または州名は何ですか。 [Unknown]: Kanagawa この単位に該当する2文字の国コードは何ですか。 [Unknown]: JP ... $ keytool -certreq -alias <認可サーバhostname> -keystore keycloak.jks > keycloak.careq
プライベート認証局で、認可サーバの証明書発行要求に署名を入れます。
$ echo subjectAltName=DNS:<認可サーバhostname> > san.ext $ openssl ca -in keycloak.careq -out keycloak.crt -days 3650 -cert ca.crt -keyfile ca.key -extfile san.ext
認可サーバで、CA証明書および認可サーバのサーバ証明書をキーストアにインポートします。
$ keytool -import -keystore keycloak.jks -file ca.crt -alias root $ keytool -import -alias <認可サーバhostname> -keystore keycloak.jks -file keycloak.crt
Keycloakの構成定義ファイル(keycloak-9.0.3/standalone/configuration/standalone.xml)を変更し、キーストアを指定します。また、ここではKeycloakのHTTPSのポートとして443を使いますので、併せて指定します。
<management> <security-realms> ... <security-realm name="ApplicationRealm"> <server-identities> <ssl> <keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret" alias="<認可サーバhostname>" key-password="secret" generate-self-signed-certificate-host="<認可サーバhostname>"/> </ssl> </server-identities> ... </security-realm> </security-realms> ... </management> ... <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}"> ... <socket-binding name="https" port="${jboss.https.port:443}"/> ... </socket-binding-group>
次に、NGINXとの通信をTLSで暗号化してみましょう。APIゲートウェイで、opensslコマンドを用いて秘密鍵および証明書発行要求を作成します。
$ sudo openssl genrsa -out nginx.key 2048 $ sudo openssl req -new -key nginx.key -out nginx.csr ... Country Name (2 letter code) [XX]:JP State or Province Name (full name) []:Kanagawa Locality Name (eg, city) [Default City]: Organization Name (eg, company) [Default Company Ltd]: Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:<APIゲートウェイhostname> ...
プライベート認証局で、APIゲートウェイの証明書発行要求に署名を入れます。
$ echo subjectAltName=DNS:<APIゲートウェイhostname> > san.ext $ openssl ca -in nginx.csr -out nginx.crt -days 3650 -cert ca.crt -keyfile ca.key -extfile san.ext
APIゲートウェイで、構成定義ファイル(/etc/nginx/conf.d/sample.conf)を変更し、秘密鍵とサーバ証明書を指定します。また、ここではNGINXのHTTPSのポートとして443を使いますので、併せて指定します。
server { listen 443 ssl; ... ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ... location = /_oauth2_send_request { ... proxy_pass https://<認可サーバhostname>/auth/realms/sample_service/protocol/openid-connect/token/introspect; } }
通信をTLSで暗号化するための設定は以上で完了です。動作確認をしてみましょう。curlコマンドを用いる場合、cacertオプションにプライベート認証局のCA証明書(ca.crt)を付与します。
$ export access_token=$(curl --cacert ca.crt https://<認可サーバhostname>/auth/realms/sample_service/protocol/openid-connect/token -d "grant_type=password&client_id=sample_application&client_secret=<client secret>&username=sample_user&password=<password>&scope=openid" | jq -r '.access_token') $ curl --cacert ca.crt https://<APIゲートウェイhostname>/echo -H "Authorization: Bearer $access_token" Hello!
TLSでの暗号化に成功しました。
ハードニング②:Authorization Code Grant
次に、OAuth 2.0の認可フローをResource Owner Password Credentials GrantからAuthorization Code Grantに変更する方法です(図3)。Authorization Code Grantに変更することで、ユーザの秘密情報が外部アプリを経由することがなくなるので、外部アプリからユーザの秘密情報を取得できなくなります。
実際に設定していきます。まずは、Authorization Code Grantに対応する外部アプリを作ります。外部アプリのソースコードは、GitHubに公開していますので、そこからダウンロードします。
まずは、外部アプリとの通信をTLSで暗号化するために、外部アプリで、keytoolコマンドを用いてキーストアおよび証明書発行要求を作成します。
$ keytool -genkey -alias <外部アプリhostname> -keyalg RSA -keystore application.jks -validity 10950 キーストアのパスワードを入力してください: secret 新規パスワードを再入力してください: secret 姓名は何ですか。 [Unknown]: <外部アプリhostname> 組織単位名は何ですか。 [Unknown]: 組織名は何ですか。 [Unknown]: Default Company Ltd 都市名または地域名は何ですか。 [Unknown]: 都道府県名または州名は何ですか。 [Unknown]: Kanagawa この単位に該当する2文字の国コードは何ですか。 [Unknown]: JP ... $ keytool -certreq -alias <外部アプリhostname> -keystore application.jks > application.careq
プライベート認証局で、外部アプリの証明書発行要求に署名を入れます。
$ echo subjectAltName=DNS:<外部アプリhostname> > san.ext $ openssl ca -in application.careq -out application.crt -days 3650 -cert ca.crt -keyfile ca.key -extfile san.ext
外部アプリで、CA証明書および外部アプリのサーバ証明書をキーストアにインポートします。
$ keytool -import -keystore application.jks -file ca.crt -alias root $ keytool -import -alias <外部アプリhostname> -keystore application.jks -file application.crt
作成したキーストアを外部アプリのsrc/main/resources/keystoreに配置します。次に、Keycloakの外部アプリ用のクライアントの設定を変更します。Authorization Code Grantに変更するために、管理コンソールで外部アプリ用のクライアントの設定項目を以下の通り変更します(表1、図4)。また、ここでは外部アプリのHTTPSのポートとして443を使います。
設定項目名 | 説明 | 変更前 | 変更後 |
---|---|---|---|
Standard Flow Enabled | Authorization Code Grantの有効性 | OFF | ON |
Direct Access Grants Enabled | Resource Owner Password Credentials Grantの有効性 | ON | OFF |
Valid Redirect URIs | 認可コードを受け取るためのコールバックエンドポイント。セキュリティを考慮する上で以下に注意する - ワイルドカードは使わないこと - 連携する認可サーバが複数ある場合は認可サーバごとにエンドポイントを変えること | 非表示 | https://<外部アプリhostname>/gettoken |
次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。
server.ssl.key-alias=<外部アプリhostname> ... clientapp.config.keycloak-url=https://<認可サーバhostname> clientapp.config.apiserver-url=https://<APIゲートウェイhostname> clientapp.config.clientapp-url=https://<外部アプリhostname> ... clientapp.config.client-secret=<client secret>
外部アプリを起動します。
$ cd ./clientapp $ mvn spring-boot:run
https://<外部アプリhostname>/にアクセスすると、外部アプリのトップ画面が表示されます(図5)。
動作確認をしてみましょう。外部アプリのトップ画面にある[Get Token]をクリックすると、Keycloakの認可エンドポイントをコールし、Keycloakのログイン画面が表示されます(図6)。
Keycloakのログイン画面で、ユーザ名とパスワード(ここでは「sample_user」とそのパスワード)を入力し、[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図7)。
外部アプリのトークン取得画面で、[back]をクリックすると、再び外部アプリのトップ画面が表示されます(図8)。このとき、取得したアクセストークン(の一部)が[Token]の欄に表示されます。
外部アプリのトップ画面で、[Call Echo API]ボタンをクリックすると、アクセストークンとともにEcho APIをコールし、Echo APIのレスポンスである「Hello!」が[Result]の欄に表示されます(図9)。
OAuth 2.0の認可フローを、Resource Owner Password Credentials GrantからAuthorization Code Grantに変更できました。
ハードニング③:scope
次に、アクセストークンのscopeクレームを用いる方法です(図10)。scopeクレームを用いることで、外部アプリは許可された範囲外のリソースを操作できなくなります。
実際に設定していきます。ここでは、Echo APIをコールするために、「greeting」というスコープが必要ということにしましょう。APIゲートウェイで、oauth2.js(/etc/nginx/oauth2.js)を変更し、scopeクレームに「greeting」を含まない場合は、403を返すようにします。
function introspectAccessToken(r) { r.subrequest("/_oauth2_send_request", function(reply) { if (reply.status == 200) { var response = JSON.parse(reply.responseBody); if (response.active == true) { // scope var scope = new RegExp("greeting"); if (!scope.test(response.scope)) { r.return(403); return; } r.return(204); ... } ... } } ); }
上記設定により、外部アプリのトップ画面で、[Call Echo API]ボタンをクリックすると、「403 FORBIDDEN」が[Result]の欄に表示され、既存の設定ではEcho APIにアクセスできなくなります(図11)。
外部アプリがEcho APIにアクセスできるようにするために、まずはgreetingスコープを作成します。Client Scopesタブの[Create]ボタンをクリックすると、Add client scope画面が表示されますので(図12)、スコープ名(ここでは「greeting」)を入力し、[Save]ボタンをクリックします。
次に、外部アプリがgreetingスコープを要求できるように設定します。外部アプリのクライアント設定画面のClient Scopesタブで、greetingスコープをAssigned Optional Client Scopesに追加します(図13)。この設定によって、認可リクエスト時scopeパラメータにgreetingを指定することで、アクセストークンのscopeクレームにgreetingが設定されるようになります。
次に、外部アプリがgreetingスコープを要求してきた際に、ユーザに同意をとるように設定します。外部アプリのクライアント設定画面のSettingsタブで、Consent RequiredをONにします(図14)。
次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時scopeパラメータにgreetingを指定するようになります。
clientapp.config.scope=openid greeting
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、同意画面が表示されます(図15)。同意画面で、外部アプリがgreetingスコープを要求していることを確認できます。[Yes]ボタンをクリックします。
外部アプリのトークン取得画面でアクセストークンを確認したのち、[back]をクリックして外部アプリのトップ画面に戻り、[Call Echo API]ボタンをクリックすると、アクセスが許可され、「Hello!」が[Result]の欄に表示されます(図16)。
scopeクレームを用いたアクセス制御ができました。
ハードニング④:aud
次に、アクセストークンのaud(Audience)クレームを用いる方法です(図17)。audクレームを用いることで、ユーザが許可していないAPIゲートウェイはリソースを操作しなくなります。
実際に設定していきます。ここでは、Echo APIを操作するAPIゲートウェイをsample_api_gatewayに限定します。APIゲートウェイで、oauth2.js(/etc/nginx/oauth2.js)を変更し、audクレームに「sample_api_gateway」を含まない場合は、403を返すようにします。
function introspectAccessToken(r) { r.subrequest("/_oauth2_send_request", function(reply) { if (reply.status == 200) { var response = JSON.parse(reply.responseBody); if (response.active == true) { // aud var aud = new RegExp("sample_api_gateway"); if (!aud.test(response.aud)) { r.return(403); return; } // scope ... } .. } } ); }
上記設定により、外部アプリのトップ画面で、[Call Echo API]ボタンをクリックすると、「403 FORBIDDEN」が[Result]の欄に表示され、既存の設定ではEcho APIにアクセスできなくなります(図18)。
外部アプリがEcho APIにアクセスできるようにするために、まずはsample_api_gatewayスコープを作成します。Client Scopesタブの[Create]ボタンをクリックすると、Add client scope画面が表示されますので(図19)、スコープ名(ここでは「sample_api_gateway」)を入力し、[Save]ボタンをクリックします。
次に、sample_api_gatewayスコープ付与時に、audクレームにsample_api_gatewayが設定されるように、Protocol Mapper(sample_api_gateway-audience)を作成します。Client ScopesタブのMappersタブで[Create]ボタンをクリックすると、Create Protocol Mapper画面が表示されますので(図20)、以下の通り設定したProtocol Mapperを作成します(表2)。
設定項目名 | 説明 | 設定値 |
---|---|---|
Name | Mapperの名前 | sample_api_gateway-audience |
Mapper Type | Mapperのタイプ。「Audience」を選択すると、audクレームの設定ができる | Audience |
Included Client Audience | audクレームに含めるクライアントのクライアントID | sample_api_gateway |
Add to access token | アクセストークンに本Mapperに対応するクレームを追加するかどうか | ON |
次に、外部アプリがsample_api_gatewayスコープを要求できるように設定します。外部アプリのクライアント設定画面のClient Scopesタブで、sample_api_gatewayスコープをAssigned Optional Client Scopesに追加します(図21)。この設定によって、認可リクエスト時scopeパラメータにsample_api_gatewayを指定することで、アクセストークンのaudクレームにsample_api_gatewayが設定されるようになります。
次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時scopeパラメータにsample_api_gatewayを指定するようになります。
clientapp.config.scope=openid greeting sample_api_gateway
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、同意画面が表示されます(図22)。同意画面で、外部アプリがアクセストークンを使ってAPIゲートウェイ(sample_api_gateway)を呼ぼうとしていることを確認できます。[Yes]ボタンをクリックします。
外部アプリのトークン取得画面でアクセストークンを確認したのち、[back]をクリックして外部アプリのトップ画面に戻り、[Call Echo API]ボタンをクリックするとアクセスが許可され、「Hello!」が[Result]の欄に表示されます(図23)。
audクレームを用いたアクセス制御ができました。
ハードニング⑤:OAuth MTLS
次に、OAuth MTLS(OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens: RFC8705)を用いる方法です(図24)。OAuth MTLSを用いることで、トークンを受け取った外部アプリ以外からのAPIコールに対して、APIゲートウェイはリソースを操作しなくなります。
実際に設定していきます。ここでは、Echo APIをコールできる外部アプリをsample_applicationに限定します。APIゲートウェイで、構成定義ファイル(/etc/nginx/conf.d/sample.conf)を変更し、APIゲートウェイが外部アプリのクライアント証明書を受け取れるようにします。
server { ... ssl_client_certificate /etc/nginx/ssl/ca.crt; ssl_verify_client on; ... }
上記設定により、NGINXの$ssl_client_raw_certという変数で、APIゲートウェイにアクセスしてきた外部アプリのクライアント証明書(PEM形式)を取得できるようになります。
次に、APIゲートウェイで、oauth2.js(/etc/nginx/oauth2.js)を変更し、cnfクレームのx5t#S256ヘッダパラメータに、クライアント証明書のハッシュ値を含まない場合は、403を返すようにします。本来であれば、RFC8705に記載の通り、$ssl_client_raw_cert変数で取得したクライアント証明書から算出した「クライアント証明書のハッシュ値」と、cnfクレームのx5t#S256ヘッダパラメータに格納されている値とを比較する必要があります(クライアント証明書のハッシュ値は、クライアント証明書をX.509証明書にパースし、DER形式でエンコーディングし、SHA-256でハッシュ値を算出し、base64urlでエンコードすることで算出します)。
しかしクライアント証明書のハッシュ値をNGINX JavaScript Moduleで計算するのは難しいので、ここでは以下の2点を確認することで、OAuth MTLSを用いたアクセス制御の代替とします。※
- 外部アプリのクライアント証明書のハッシュ値(すでに計算済み)がcnfクレームのx5t#S256ヘッダパラメータに格納されていること
- 外部アプリのクライアント証明書が$ssl_client_raw_cert変数に格納されていること
※ OAuth MTLSを用いたアクセス制御の正規の実装方法については、筆者が3scaleのAPIゲートウェイであるAPIcastにLua言語で実装したOAuth MTLSポリシーの実装(https://github.com/3scale/APIcast/blob/master/gateway/src/apicast/policy/oauth_mtls/oauth_mtls.lua)が参考になります。またAPIゲートウェイとして、NGINXの代わりにRed Hat Fuseを用いるという方法も代替手段の1つです。Red Hat Fuseを使えば、OAuth MTLSのロジックをKeycloakと同様Javaで実装することができます。
function introspectAccessToken(r) { r.subrequest("/_oauth2_send_request", function(reply) { if (reply.status == 200) { var response = JSON.parse(reply.responseBody); if (response.active == true) { // OAuth MTLS var cnf = new RegExp("nzlMOyV2hvK8yQmot5lWe2RGrYJG2g0d9y-Tvz6EgxA"); if (!response.cnf) { r.return(403); return; } if (!cnf.test(response.cnf['x5t#S256'])) { r.return(403); return; } var cert = new RegExp("-----BEGIN CERTIFICATE-----\n" + "MIIDeTCCAmGgAwIBAgIBCzANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJKUDER\n" + ... "-----END CERTIFICATE-----"); if (!cert.test(r.variables.ssl_client_raw_cert)) { r.return(403); return; } // aud ... } ... } } ); }
上記設定により、外部アプリのトップ画面で、[Call Echo API]ボタンをクリックすると、「403 FORBIDDEN」が[Result]の欄に表示され、既存の設定ではEcho APIにアクセスできなくなります(図25)。
外部アプリがEcho APIにアクセスできるようにするために、Keycloakの構成定義ファイル(keycloak-9.0.3/standalone/configuration/standalone.xml)を変更し、Keycloakが外部アプリのクライアント証明書を受け取れるようにします。
<management> <security-realms> ... <security-realm name="ApplicationRealm"> ... <authentication> <truststore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret"/> … </authentication> ... </security-realm> </security-realms> ... </management> ... <profile> ... <subsystem xmlns="urn:jboss:domain:undertow:10.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}"> ... <server name="default-server"> <https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true" verify-client="REQUESTED"/> ... </server> ... </subsystem> ... </profile>
次に、cnfクレームのx5t#S256ヘッダパラメータに、クライアント証明書のハッシュ値が設定されるように、外部アプリのクライアント設定画面のSettingsタブで、OAuth 2.0 Mutual TLS Certificate Bound Access Tokens EnabledをONにします(図26)。
動作を確認してみましょう。外部アプリでトークンを取得したのち、[Call Echo API]ボタンをクリックするとアクセスが許可され、「Hello!」が[Result]の欄に表示されます(図27)。
簡易的ではありますが、OAuth MTLSを用いたアクセス制御ができました。
ハードニング⑥:state
次に、stateパラメータを用いる方法です(図28)。stateパラメータを用いることで、外部アプリは認可リクエストと認可レスポンスを同一セッションとしてバインドすることができ、外部アプリは対応する認可リクエストのない認可レスポンスを処理しなくなります。
実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時stateパラメータに都度生成した乱数を指定するようになり、また認可レスポンスのstateパラメータに指定された値と比較して、認可リクエストと認可レスポンスの対の整合性を外部アプリが確認するようになります。このように、stateパラメータを用いる場合、外部アプリ側での適切なロジックの実装が鍵となります。
oauth.config.state=true
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図29)。
トークン取得までの操作や取得したトークン自体には変化はありませんが、認可リクエストと認可レスポンスを確認すると、それぞれに同一のstateパラメータ値が指定されていることがわかります。
以上で、stateパラメータを用いた、認可リクエスト改ざんおよび認可コード盗聴への対策ができました。
ハードニング⑦:nonce
次に、nonceパラメータを用いる方法です(図30)。nonceパラメータを用いることで、外部アプリは認可リクエストとトークンレスポンスを同一セッションとしてバインドすることができ、外部アプリは対応する認可リクエストのないトークンレスポンスを用いてAPIをコールしなくなります。
実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時nonceパラメータに都度生成した乱数を指定するようになり、またIDトークンのnonceクレームに指定された値と比較して、認可リクエストとトークンレスポンスの対の整合性を外部アプリが確認するようになります。このように、nonceパラメータを用いる場合、stateパラメータを用いる場合と同様、外部アプリ側での適切なロジックの実装が鍵となります。
oauth.config.nonce=true
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図31)。
トークン取得までの操作には変化はありませんが、認可リクエストとIDトークンを確認すると、それぞれに同一のnonceパラメータ/クレーム値が指定されていることがわかります。
以上で、nonceパラメータを用いた、認可リクエスト改ざんおよび認可コード盗聴への対策ができました。
ハードニング⑧:PKCE
次に、PKCE(Proof Key for Code Exchange: RFC7636)を用いる方法です(図32)。PKCEを用いることで、認可サーバは認可リクエストとトークンリクエストを同一セッションとしてバインドすることができ、認可サーバは対応する認可リクエストのないトークンリクエストに対し、トークンを返さなくなります。
実際に設定していきます。まずは、PKCEを実装していない外部アプリに対して、トークンを払い出さないようにKeycloakを設定します。外部アプリのクライアント設定画面のSettingsタブで、Proof Key for Code Exchange Code Challenge Methodに「S256」を設定します(図33)。code challenge methodには「S256」か「plain」を指定できますが、「plain」を指定すると、code challengeがcode verifierと同じ値になり、セキュリティ強度が下がってしまいます。ここでは、OAuth 2.0のセキュリティベストプラクティスでも推奨されている「S256」を指定しましょう。
上記設定により、外部アプリのトップ画面で、[Get Token]ボタンをクリックすると、エラー画面が表示され、既存の設定ではKeycloakのログイン画面にアクセスできなくなります(図34)。
認可レスポンスが以下のように返ってくることからも、外部アプリにPKCEの実装が足りてないことがわかります。
https://<外部アプリhostname>/gettoken?error=invalid_request&error_description=Missing+parameter%3A+code_challenge_method& state=ffeced7c-3db0-4d28-bba0-cba64b1626a0
外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時code_challenge_methodパラメータにS256、code_challengeパラメータにcode verifierから計算したハッシュ値を指定するようになり、またトークンリクエスト時code_verifierパラメータに256ビット以上のエントロピーを持つ乱数を指定するようになります。このように、PKCEを用いる場合、stateパラメータやnonceパラメータを用いる場合と同様、外部アプリ側での適切なロジックの実装が鍵となります。
oauth.config.pkce=true
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図35)。
トークン取得までの操作や取得したトークン自体には変化はありませんが、認可リクエストとトークンリクエストを確認すると、code_challenge_methodパラメータやcode_challengeパラメータ、code_verifierパラメータが指定されていることがわかります。
以上で、PKCEを用いた、認可リクエスト改ざんおよび認可コード盗聴への対策ができました。
ハードニング⑨:OAuth 2.0 Form Post Response Mode
最後に、OAuth 2.0 Form Post Response Modeを用いる方法です(図36)。Form Post Response Modeを用いることで、クエリ部分ではなく、リクエストボディ部分に認可コードが記載されるため、認可コードが盗聴されにくくなります。
実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時response_modeパラメータにform_postを指定するようになり、外部アプリは認可レスポンスをPOSTで受け取るようになります。
oauth.config.form-post=true
動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図37)。
トークン取得までの操作や取得したトークン自体には変化はありませんが、認可レスポンスを確認すると、認可レスポンスがPOSTで外部アプリに送られていることがわかります。
以上で、OAuth 2.0 Form Post Response Modeを用いた、認可コード盗聴への対策ができました。
次回は、コンテナ上のマイクロサービスの認証強化について、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能を紹介します。
連載バックナンバー
Think ITメルマガ会員登録受付中
全文検索エンジンによるおすすめ記事
- Keycloakのインストールと構築例
- APIセキュリティのハードニング
- コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~
- FAPI 1.0に準拠したクライアントアプリケーションと リソースサーバの作り方
- Oracle Cloud Hangout Cafe Season4 #4「マイクロサービスの認証・認可とJWT」(2021年7月7日開催)
- FAPIとKeycloakの概要
- 3scaleのAPIゲートウェイの機能を拡張してみよう!
- 3scaleの基本的な使い方
- KeycloakによるAPIセキュリティの基本
- コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~