Keycloakを用いたハードニングの実装方法

2020年9月24日(木)
田畑 義之
連載4回目となる今回は、Keycloakを用いたハードニングの実装方法を紹介します。

第四回は、第三回で紹介したハードニング方法を、実際にKeycloakを用いて実装し、動作を確認していきます。使用するシステムは、第二回で構築したもの(図1)を用います。

図1:システム構成

図1:システム構成

ハードニング①:TLS

まずは、通信をTLSで暗号化する方法です(図2)。通信をTLSで暗号化することで、盗聴が格段に難しくなります。

図2:TLSで暗号化

図2:TLSで暗号化

実際に設定していきます。通信をTLSで暗号化するために、ここではプライベート認証局を設けてサーバ証明書に署名を入れます。本番環境では信頼のおけるパブリック認証局に署名をもらってください。まずは、任意のホストをプライベート認証局として準備します。keytoolコマンドを用いて作成した証明書発行要求に署名できるように、opensslの設定ファイル(/etc/pki/tls/openssl.cnf)を変更します。

リスト1:

#string_mask = utf8only
string_mask = pkix

opensslコマンドを用いてプライベート認証局の秘密鍵とCA証明書を作成します。

リスト2:

$ 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

署名に必要なファイルを準備します。

リスト3:

$ sudo touch /etc/pki/CA/index.txt
$ echo '00' | sudo tee /etc/pki/CA/serial

以上で、プライベート認証局の完成です。

次に、Keycloakとの通信をTLSで暗号化してみましょう。認可サーバで、keytoolコマンドを用いてキーストアおよび証明書発行要求を作成します。

リスト4:

$ 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

プライベート認証局で、認可サーバの証明書発行要求に署名を入れます。

リスト5:

$ 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証明書および認可サーバのサーバ証明書をキーストアにインポートします。

リスト6:

$ 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を使いますので、併せて指定します。

リスト7:

    <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コマンドを用いて秘密鍵および証明書発行要求を作成します。

リスト8:

$ 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ゲートウェイの証明書発行要求に署名を入れます。

リスト9:

$ 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を使いますので、併せて指定します。

リスト10:

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)を付与します。

リスト11:

$ 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に変更することで、ユーザの秘密情報が外部アプリを経由することがなくなるので、外部アプリからユーザの秘密情報を取得できなくなります。

図3:Authorization Code Grant

図3:Authorization Code Grant

実際に設定していきます。まずは、Authorization Code Grantに対応する外部アプリを作ります。外部アプリのソースコードは、GitHubに公開していますので、そこからダウンロードします。

まずは、外部アプリとの通信をTLSで暗号化するために、外部アプリで、keytoolコマンドを用いてキーストアおよび証明書発行要求を作成します。

リスト12:

$ 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

プライベート認証局で、外部アプリの証明書発行要求に署名を入れます。

リスト13:

$ 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証明書および外部アプリのサーバ証明書をキーストアにインポートします。

リスト14:

$ 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を使います。

表1:外部アプリ用のクライアントの設定項目

設定項目名説明変更前変更後
Standard Flow EnabledAuthorization Code Grantの有効性OFFON
Direct Access Grants EnabledResource Owner Password Credentials Grantの有効性ONOFF
Valid Redirect URIs認可コードを受け取るためのコールバックエンドポイント。セキュリティを考慮する上で以下に注意する
- ワイルドカードは使わないこと
- 連携する認可サーバが複数ある場合は認可サーバごとにエンドポイントを変えること
非表示https://<外部アプリhostname>/gettoken
図4:ClientsタブのSettingsタブ

図4:ClientsタブのSettingsタブ

次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。

リスト15:

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>

外部アプリを起動します。

リスト16:

$ cd ./clientapp
$ mvn spring-boot:run

https://<外部アプリhostname>/にアクセスすると、外部アプリのトップ画面が表示されます(図5)。

図5:外部アプリのトップ画面

図5:外部アプリのトップ画面

動作確認をしてみましょう。外部アプリのトップ画面にある[Get Token]をクリックすると、Keycloakの認可エンドポイントをコールし、Keycloakのログイン画面が表示されます(図6)。

図6:Keycloakのログイン画面

図6:Keycloakのログイン画面

Keycloakのログイン画面で、ユーザ名とパスワード(ここでは「sample_user」とそのパスワード)を入力し、[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図7)。

図7:外部アプリのトークン取得画面

図7:外部アプリのトークン取得画面

外部アプリのトークン取得画面で、[back]をクリックすると、再び外部アプリのトップ画面が表示されます(図8)。このとき、取得したアクセストークン(の一部)が[Token]の欄に表示されます。

図8:外部アプリのトップ画面(アクセストークン取得後)

図8:外部アプリのトップ画面(アクセストークン取得後)

外部アプリのトップ画面で、[Call Echo API]ボタンをクリックすると、アクセストークンとともにEcho APIをコールし、Echo APIのレスポンスである「Hello!」が[Result]の欄に表示されます(図9)。

図9:外部アプリのトップ画面(Echo APIコール後)

図9:外部アプリのトップ画面(Echo APIコール後)

OAuth 2.0の認可フローを、Resource Owner Password Credentials GrantからAuthorization Code Grantに変更できました。

ハードニング③:scope

次に、アクセストークンのscopeクレームを用いる方法です(図10)。scopeクレームを用いることで、外部アプリは許可された範囲外のリソースを操作できなくなります。

図10: scope

図10: scope

実際に設定していきます。ここでは、Echo APIをコールするために、「greeting」というスコープが必要ということにしましょう。APIゲートウェイで、oauth2.js(/etc/nginx/oauth2.js)を変更し、scopeクレームに「greeting」を含まない場合は、403を返すようにします。

リスト17:

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)。

図11:外部アプリのトップ画面(403 FORBIDDEN)

図11:外部アプリのトップ画面(403 FORBIDDEN)

外部アプリがEcho APIにアクセスできるようにするために、まずはgreetingスコープを作成します。Client Scopesタブの[Create]ボタンをクリックすると、Add client scope画面が表示されますので(図12)、スコープ名(ここでは「greeting」)を入力し、[Save]ボタンをクリックします。

図12:Add client scope画面

図12:Add client scope画面

次に、外部アプリがgreetingスコープを要求できるように設定します。外部アプリのクライアント設定画面のClient Scopesタブで、greetingスコープをAssigned Optional Client Scopesに追加します(図13)。この設定によって、認可リクエスト時scopeパラメータにgreetingを指定することで、アクセストークンのscopeクレームにgreetingが設定されるようになります。

図13:ClientsタブのClient Scopesタブ

図13:ClientsタブのClient Scopesタブ

次に、外部アプリがgreetingスコープを要求してきた際に、ユーザに同意をとるように設定します。外部アプリのクライアント設定画面のSettingsタブで、Consent RequiredをONにします(図14)。

図14:ClientsタブのSettingsタブ

図14:ClientsタブのSettingsタブ

次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時scopeパラメータにgreetingを指定するようになります。

リスト18:

clientapp.config.scope=openid greeting

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、同意画面が表示されます(図15)。同意画面で、外部アプリがgreetingスコープを要求していることを確認できます。[Yes]ボタンをクリックします。

図15:同意画面

図15:同意画面

外部アプリのトークン取得画面でアクセストークンを確認したのち、[back]をクリックして外部アプリのトップ画面に戻り、[Call Echo API]ボタンをクリックすると、アクセスが許可され、「Hello!」が[Result]の欄に表示されます(図16)。

図16;外部アプリのトップ画面(Echo APIコール後)

図16;外部アプリのトップ画面(Echo APIコール後)

scopeクレームを用いたアクセス制御ができました。

ハードニング④:aud

次に、アクセストークンのaud(Audience)クレームを用いる方法です(図17)。audクレームを用いることで、ユーザが許可していないAPIゲートウェイはリソースを操作しなくなります。

図17:aud

図17:aud

実際に設定していきます。ここでは、Echo APIを操作するAPIゲートウェイをsample_api_gatewayに限定します。APIゲートウェイで、oauth2.js(/etc/nginx/oauth2.js)を変更し、audクレームに「sample_api_gateway」を含まない場合は、403を返すようにします。

リスト19:

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)。

図18:外部アプリのトップ画面(403 FORBIDDEN)

図18:外部アプリのトップ画面(403 FORBIDDEN)

外部アプリがEcho APIにアクセスできるようにするために、まずはsample_api_gatewayスコープを作成します。Client Scopesタブの[Create]ボタンをクリックすると、Add client scope画面が表示されますので(図19)、スコープ名(ここでは「sample_api_gateway」)を入力し、[Save]ボタンをクリックします。

図19:Add client scope画面

図19:Add client scope画面

次に、sample_api_gatewayスコープ付与時に、audクレームにsample_api_gatewayが設定されるように、Protocol Mapper(sample_api_gateway-audience)を作成します。Client ScopesタブのMappersタブで[Create]ボタンをクリックすると、Create Protocol Mapper画面が表示されますので(図20)、以下の通り設定したProtocol Mapperを作成します(表2)。

表2:Protocol Mapperの設定項目

設定項目名説明設定値
NameMapperの名前sample_api_gateway-audience
Mapper TypeMapperのタイプ。「Audience」を選択すると、audクレームの設定ができるAudience
Included Client Audienceaudクレームに含めるクライアントのクライアントIDsample_api_gateway
Add to access tokenアクセストークンに本Mapperに対応するクレームを追加するかどうかON
図20:Create Protocol Mapper画面

図20:Create Protocol Mapper画面

次に、外部アプリがsample_api_gatewayスコープを要求できるように設定します。外部アプリのクライアント設定画面のClient Scopesタブで、sample_api_gatewayスコープをAssigned Optional Client Scopesに追加します(図21)。この設定によって、認可リクエスト時scopeパラメータにsample_api_gatewayを指定することで、アクセストークンのaudクレームにsample_api_gatewayが設定されるようになります。

図21:ClientsタブのClient Scopesタブ

図21:ClientsタブのClient Scopesタブ

次に、外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時scopeパラメータにsample_api_gatewayを指定するようになります。

リスト20:

clientapp.config.scope=openid greeting sample_api_gateway

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、同意画面が表示されます(図22)。同意画面で、外部アプリがアクセストークンを使ってAPIゲートウェイ(sample_api_gateway)を呼ぼうとしていることを確認できます。[Yes]ボタンをクリックします。

図22:同意画面

図22:同意画面

外部アプリのトークン取得画面でアクセストークンを確認したのち、[back]をクリックして外部アプリのトップ画面に戻り、[Call Echo API]ボタンをクリックするとアクセスが許可され、「Hello!」が[Result]の欄に表示されます(図23)。

図23:外部アプリのトップ画面(Echo APIコール後)

図23:外部アプリのトップ画面(Echo APIコール後)

audクレームを用いたアクセス制御ができました。

ハードニング⑤:OAuth MTLS

次に、OAuth MTLS(OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens: RFC8705)を用いる方法です(図24)。OAuth MTLSを用いることで、トークンを受け取った外部アプリ以外からのAPIコールに対して、APIゲートウェイはリソースを操作しなくなります。

図24:OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

図24:OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

実際に設定していきます。ここでは、Echo APIをコールできる外部アプリをsample_applicationに限定します。APIゲートウェイで、構成定義ファイル(/etc/nginx/conf.d/sample.conf)を変更し、APIゲートウェイが外部アプリのクライアント証明書を受け取れるようにします。

リスト21:

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を用いたアクセス制御の代替とします。

  1. 外部アプリのクライアント証明書のハッシュ値(すでに計算済み)がcnfクレームのx5t#S256ヘッダパラメータに格納されていること
  2. 外部アプリのクライアント証明書が$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で実装することができます。

リスト22:

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)。

図25:外部アプリのトップ画面(403 FORBIDDEN)

図25:外部アプリのトップ画面(403 FORBIDDEN)

外部アプリがEcho APIにアクセスできるようにするために、Keycloakの構成定義ファイル(keycloak-9.0.3/standalone/configuration/standalone.xml)を変更し、Keycloakが外部アプリのクライアント証明書を受け取れるようにします。

リスト23:

    <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)。

図26:ClientsタブのSettingsタブ

図26:ClientsタブのSettingsタブ

動作を確認してみましょう。外部アプリでトークンを取得したのち、[Call Echo API]ボタンをクリックするとアクセスが許可され、「Hello!」が[Result]の欄に表示されます(図27)。

図27:外部アプリのトップ画面(Echo APIコール後)

図27:外部アプリのトップ画面(Echo APIコール後)

簡易的ではありますが、OAuth MTLSを用いたアクセス制御ができました。

ハードニング⑥:state

次に、stateパラメータを用いる方法です(図28)。stateパラメータを用いることで、外部アプリは認可リクエストと認可レスポンスを同一セッションとしてバインドすることができ、外部アプリは対応する認可リクエストのない認可レスポンスを処理しなくなります。

図28:state

図28:state

実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時stateパラメータに都度生成した乱数を指定するようになり、また認可レスポンスのstateパラメータに指定された値と比較して、認可リクエストと認可レスポンスの対の整合性を外部アプリが確認するようになります。このように、stateパラメータを用いる場合、外部アプリ側での適切なロジックの実装が鍵となります。

リスト24:

oauth.config.state=true

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図29)。

図29:外部アプリのトークン取得画面

図29:外部アプリのトークン取得画面

トークン取得までの操作や取得したトークン自体には変化はありませんが、認可リクエストと認可レスポンスを確認すると、それぞれに同一のstateパラメータ値が指定されていることがわかります。

以上で、stateパラメータを用いた、認可リクエスト改ざんおよび認可コード盗聴への対策ができました。

ハードニング⑦:nonce

次に、nonceパラメータを用いる方法です(図30)。nonceパラメータを用いることで、外部アプリは認可リクエストとトークンレスポンスを同一セッションとしてバインドすることができ、外部アプリは対応する認可リクエストのないトークンレスポンスを用いてAPIをコールしなくなります。

図30:nonce

図30:nonce

実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時nonceパラメータに都度生成した乱数を指定するようになり、またIDトークンのnonceクレームに指定された値と比較して、認可リクエストとトークンレスポンスの対の整合性を外部アプリが確認するようになります。このように、nonceパラメータを用いる場合、stateパラメータを用いる場合と同様、外部アプリ側での適切なロジックの実装が鍵となります。

リスト25:

oauth.config.nonce=true

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図31)。

図31: 外部アプリのトークン取得画面

図31: 外部アプリのトークン取得画面

トークン取得までの操作には変化はありませんが、認可リクエストとIDトークンを確認すると、それぞれに同一のnonceパラメータ/クレーム値が指定されていることがわかります。

以上で、nonceパラメータを用いた、認可リクエスト改ざんおよび認可コード盗聴への対策ができました。

ハードニング⑧:PKCE

次に、PKCE(Proof Key for Code Exchange: RFC7636)を用いる方法です(図32)。PKCEを用いることで、認可サーバは認可リクエストとトークンリクエストを同一セッションとしてバインドすることができ、認可サーバは対応する認可リクエストのないトークンリクエストに対し、トークンを返さなくなります。

図32:PKCE

図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」を指定しましょう。

図33: ClientsタブのSettingsタブ

図33: ClientsタブのSettingsタブ

上記設定により、外部アプリのトップ画面で、[Get Token]ボタンをクリックすると、エラー画面が表示され、既存の設定ではKeycloakのログイン画面にアクセスできなくなります(図34)。

図34:外部アプリのトークン取得画面(400 Bad Request)

図34:外部アプリのトークン取得画面(400 Bad Request)

認可レスポンスが以下のように返ってくることからも、外部アプリにPKCEの実装が足りてないことがわかります。

リスト26:

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パラメータを用いる場合と同様、外部アプリ側での適切なロジックの実装が鍵となります。

リスト27:

oauth.config.pkce=true

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図35)。

図35:外部アプリのトークン取得画面

図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を用いることで、クエリ部分ではなく、リクエストボディ部分に認可コードが記載されるため、認可コードが盗聴されにくくなります。

図36:OAuth 2.0 Form Post Response Mode

図36:OAuth 2.0 Form Post Response Mode

実際に設定していきます。外部アプリの設定ファイル(src/main/resources/application.properties)を変更します。この設定によって、認可リクエスト時response_modeパラメータにform_postを指定するようになり、外部アプリは認可レスポンスをPOSTで受け取るようになります。

リスト28:

oauth.config.form-post=true

動作を確認してみましょう。外部アプリのトップ画面で[Get Token]をクリックし、Keycloakのログイン画面でユーザ名とパスワードを入力して[Log In]ボタンをクリックすると、認可コードが外部アプリに渡され、その認可コードを用いて取得したアクセストークンがトークン取得画面に表示されます(図37)。

図37:外部アプリのトークン取得画面

図37:外部アプリのトークン取得画面

トークン取得までの操作や取得したトークン自体には変化はありませんが、認可レスポンスを確認すると、認可レスポンスがPOSTで外部アプリに送られていることがわかります。

以上で、OAuth 2.0 Form Post Response Modeを用いた、認可コード盗聴への対策ができました。

次回は、コンテナ上のマイクロサービスの認証強化について、内部向けのAPIであるマイクロサービスの認証を強化する最先端の機能を紹介します。

株式会社 日立製作所
OSSソリューションセンタにて、API管理や認証周りのOSSの開発/サポート/普及活動に従事。3scaleおよびkeycloakコミュニティのコントリビュータであり、多数のコードをコミットしている。

連載バックナンバー

運用・管理技術解説
第7回

コンテナ上のマイクロサービスの認証強化 ~StrimziとKeycloak~

2021/2/16
前回に引き続き、マイクロサービスの認証強化を実現する最先端の機能を紹介します。
運用・管理技術解説
第6回

コンテナ上のマイクロサービスの認証強化 ~QuarkusとKeycloak~

2021/1/19
連載6回目となる今回は、マイクロサービスの認証強化を実現する最先端の機能を紹介します。
セキュリティ技術解説
第5回

コンテナ上のマイクロサービスの認証強化 ~IstioとKeycloak~

2020/12/15
連載5回目となる今回は、Istioを用いたマイクロサービスのシステムをKeycloakを用いて認証強化する手順を紹介します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています