Keycloakでリバプロ型構成を組んでみる（mod_auth_openidc編）

# 今日やること

Keycloakアドベンドカレンダー8日目の今回は、ApacheでOpenID Connect連携を行うモジュール(mod_auth_openidc)を試してみます。

mod_auth_openidcは、OpenID ConnectのRelying Party(RP)としての機能をApache 2.x系に組み込むための拡張モジュールです。mod_auth_openidcをApacheに組み込むことにより、OpenID Connect Provider(OP)とID連携を可能にします。

既存のApacheで、

 - Apache自体で動作するアプリケーション
 - Apacheがリバースプロキシしているアプリケーション

のどちらにおいても、コンテンツの保護および、ID連携を可能にします。
Apacheの基本機能はそのまま利用可能なので、複数のリバースプロキシ先を指定したり、URL置換やヘッダ調整、アクセス制御などを組み合わせられるメリットがあります。

OPとID連携を行ったユーザーIDはApacheの`REMOTE_USER`環境変数に連携されるようになります。その他のユーザー情報に関しては、IDトークン内claimの中に含まれているものであればHTTPヘッダから取得することが可能です。

今回は、CentOS 7の標準パッケージのhttpdとmod_auth_openidcを利用して、OP(Keycloakサーバー)とID連携の動作確認を行います。

# 動作確認用のサーバー構成

今回のお試し環境はこのような感じです。
Keycloakサーバーには、`demo`レルムが作成されており([3日目の記事を参照](https://qiita.com/tamura__246/items/4290a1035e1adcab733b)、以下のようなロールをもつユーザーが存在していることが前提となります。

|FQDN|OS|JDK|構成|
|:---|:---|:---|:---|
|kc-server.example.com|CentOS 7.4.1708|OpenJDK 1.8.0.151|・Keycloakサーバー 3.3.0.CR2|
|kc-httpd.example.com|CentOS 7.4.1708|OpenJDK 1.8.0.151|・httpd 2.4.6<br>・mod_auth_openidc 1.8.8<br>・tomcat 7.0.76<br>(いずれもOS標準パッケージを使用)|

|ユーザー|ロール|
|:---|:---|
|user001|user|
|admin001|user,admin|

Tomcatに付属するexapmlesアプリケーションでは、以下のようなアクセス制御をかけることを想定します。

![httpd_acl01.png](https://qiita-image-store.s3.amazonaws.com/0/209129/a9d0332b-57e5-fc6d-5a34-68f5b8985dfd.png)


動作確認時の各サーバーへのアクセスポートは以下のとおりです。
![httpd_port01.png](https://qiita-image-store.s3.amazonaws.com/0/209129/3dc5e376-aad3-ddff-dd2e-4b47347bfb25.png)


# Keycloakサーバー側の設定

## クライアントの追加・設定
1. Keycloakの管理コンソールにログインします。
1. 左メニューバーから、`demo`レルムを選択します。
1. 左メニューバーで `クライアント` をクリックします。クライアント一覧が表示されます。
2. 右上の `作成` ボタンを押下します。
3. 以下のようなクライアント設定値を入力し、`保存` ボタンを押下します
 - クライアントID: kc-httpd
 - クライアントプロトコル: openid-connect
 - ルートURL: http://kc-httpd.example.com
![httpd01.png](https://qiita-image-store.s3.amazonaws.com/0/209129/412c88e9-ccb8-9961-0569-2ccfc7bb68d0.png)

4. 引き続き kc-tomcatの設定画面が表示されるので以下のような設定を行い、`保存` ボタンを押下します。
 - アクセスタイプ: confidential
 - 有効なリダイレクトURI: http://kc-httpd.example.com/examples/servlets/redirect_uri
![httpd02_opt.png](https://qiita-image-store.s3.amazonaws.com/0/209129/348a7eeb-86aa-bc82-cad9-949a043277fb.png)
:information_source: mod_auth_openidcを使う場合の`有効なリダイレクトURI`は、mod_auth_openidc側の設定の`OIDCRedirectURI`と同一のものを1つだけ設定します。mod_auth_openidcでは、`OIDCRedirectURI`のURLを経由してユーザーが本来アクセスしていたURLに再度リダイレクトを行うため、このURLを許可するだけで済みます。
5. `クレデンシャル` タブを押下します。
6. `シークレット` として表示されている値を控えます。
![httpd03.png](https://qiita-image-store.s3.amazonaws.com/0/209129/f6ac96c8-5ead-702f-edce-dce37f8b3157.png)


## マッパーの設定
上記の操作に引き続き、`マッパー`の設定を行います。
この設定によりIDトークンから各ユーザーにアサインされているロールの値が取得できるようになるので、後述のロールによるアクセス制御を可能にします。

7. `マッパー` タブを押下します。
8. 右上の `作成` ボタンを押下します。
9. 以下のような設定値を入力し、`保存` ボタンを押下します
 - 名前: role
 - マッパータイプ: User Realm Role
 - マルチバリュー: オン
 - トークンクレーム名: role
 - IDトークンに追加: オン
 - アクセストークンに追加: オン
 - UserInfoに追加: オン
 ![httpd04.png](https://qiita-image-store.s3.amazonaws.com/0/209129/918ae549-d495-bea6-78b1-2b2a336cf19e.png)

# mod_auth_oidcの設定

## httpdおよびmod_auth_oidc、Tomcatサンプルアプリの導入

今回はCentOS 7(7.4.1708)で、OS標準で提供されているhttpd、mod_auth_openidc、tomcat-webappsパッケージをそのまま使います。yumコマンドを使うので依存パッケージは自動でインストールされます。

```bash
yum -y install httpd mod_auth_openidc tomcat-webapps
```

## /etc/httpd/conf.d/auth_openidc_examples.conf の追加

Apacheの設定ディレクトリ内に、mod_auth_openidcの設定を追加します。
OIDC～ で始まっているパラメータがmod_auth_openidcの設定ディレクティブです。

Locationディレクティブでは、プロキシ転送設定およびアクセス制御の設定を行います。今回は以下のような設定を行っています。

- /examples/ のアクセスをTomcatにプロキシ転送
- /examples/servlets/* へのアクセスは認証のみが必要
- /examples/websocket/* へのアクセスはadminロールの権限が必要

```bash:/etc/httpd/conf.d/auth_openidc_examples.conf
NameVirtualHost *:80

<VirtualHost *:80>

    ServerName kc-httpd.example.com
    DocumentRoot /var/www/html

    OIDCResponseType code
    OIDCCryptoPassphrase {任意のパスフレーズ}

    OIDCProviderMetadataURL https://kc-server.example.com:8443/auth/realms/demo/.well-known/openid-configuration
    OIDCSSLValidateServer Off

    OIDCClientID kc-httpd
    OIDCClientSecret {Keycloakサーバーのシークレットの値}
    OIDCRedirectURI http://kc-httpd.example.com/examples/servlets/redirect_uri

    # REMOTE_USER として、claim の preferred_username を設定
    OIDCRemoteUserClaim preferred_username

    # プロキシ転送設定
    <Location /examples>
        ProxyPass http://localhost:8080/examples
        ProxyPassReverse http://localhost:8080/examples
    </Location>

    # /examples/servlets では認証が必要
    <Location /examples/servlets>
        AuthType openid-connect
        Require valid-user
    </Location>

    # /examples/websocket では IDトークンの role に admin が含まれているユーザーのみアクセスを許可
    <Location /examples/websocket>
        AuthType openid-connect
        Require claim "role~.*admin.*$"
    </Location>

</VirtualHost>
```
### mod_auth_openidc の設定

|パラメータ名 |必須|  説明  |
|:-----------------|:-----------------:|:------------------|
| OIDCResponseType|  | OpenID Connectのレスポンスタイプを<br>　(1) code<br>　(2) id_token<br>　(3) id_token token<br>　(4) code id_token<br>　(5) code token<br>　(6) code id_token token<br> の中から指定します。このレスポンスタイプの指定により、OpenID Connectの認証フローが定まります。デフォルトは、(1) の code なので、 認可コードフローになります。(2)と(3)は、Implicitフローになります。(4)～(6)はHyblidフローになります。|
| OIDCCryptoPassphrase | ○ | Cookieやキャッシュの暗号化で使用されるパスフレーズです。任意の値を設定します。  |
| OIDCProviderMetadataURL | ○ |Keycloak側のメタデータURLを指定します。|
| OIDCSSLValidateServer |  | Keycloak側との接続で、SSLを使用する場合に有効なサーバー証明書かどうかをチェックするかどうかを指定します。今回はKeycloakの自己署名サーバー証明書をプロキシサーバ側のOSの信頼済み証明書としてインポートしていないのでOffにします。プロダクション環境では、On(デフォルト)で動作させるべきです。|
| OIDCClientID | ○ | Keycloak側のクライアント設定のクライアントID(client-id)を指定します。|
| OIDCClientSecret | △ |Keycloak側のアクセスタイプで"confidential"を指定した場合は必須です。|
| OIDCRedirectURI | ○ | mod_auth_openidcで保護されているパス上のURLを指定する必要がありますが実際にコンテンツがあるURLを指定してはいけません。このURLがOIDCの動作中に利用される予約URLとなるため、ユーザーが直接呼び出すことができなくなるためです(直接呼び出した場合は、パラメーター不足で500エラーが発生します)。<br>mod_auth_openidcのOIDCの動作ではログイン成功後、まず最初にこのURLにリダイレクトされた後で、元々ユーザーがアクセスしていたURLに再度リダイレクトされる動作となります。<br>このURLのプロトコルがhttpsでないと、Apacheのerror_logに警告がでますが、今回の動作確認では特に問題ありません。ただし、OIDCではセキュリティ担保のためHTTPSの利用が前提となっています。そのため、実環境ではhttpsで始まるURLにすべきです。|
| OIDCRemoteUserClaim |  | ApacheのREMOTE_USER環境変数に設定したいIDトークンのclaim名を指定します。指定しなかった場合、REMOTE_USERは<br>　{IDトークンのsubの値}@{FQDN}:{ポート番号}/auth/realms/{レルム名} <br>という値になります。第2,3引数を使用することで、値を編集することも可能です。 |

:information_source: mod_auth_openidcの設定は非常に多くあるため、動作確認に必要な最低限の項目のみ記載しています。その他のパラメータに関しては、[参考資料の"mod_auth_openidcの設定項目一覧"](#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99) を参照ください。

### アクセス制御をかけるLocationディレクティブ内の設定

|パラメータ名 |必須|  説明  |
|:-----------------|:-----------------:|:------------------|
| AuthType |○  | "openid-connect"で固定です。   |
| Require |○  | ・valid-user: 認証が必要となる設定です<br>・claim: claimの値でアクセス制御する場合の設定です<br><br>例）<br>claim name:Joe<br>　 => name が Joe のユーザーのみ許可<br>claim "role~.\*admin.\*"<br>　 => role に admin が含まれるユーザーのみ許可  |

:information_source: claimによるアクセス制御は様々な設定方法があります。その他の方法に関しては、[参考資料の"Authorization"](#%E5%8F%82%E8%80%83%E8%B3%87%E6%96%99)をご確認ください。

## Apache／Tomcatの起動

mod_auth_openidcの準備が整ったので、Apacheおよび、Tomcatを起動します。

```bash
systemctl start tomcat
systemctl start httpd
```

# 動作確認
## 認証が必要なURL
1. 認証が必要な http://kc-httpd.example.com/examples/servlets/servlet/RequestHeaderExample にアクセスします。
2. Keycloakに未ログインであるため、ログイン画面にリダイレクトが発生します。
3. ID/PWでログインします。
5. http://kc-httpd.example.com/examples/servlets/servlet/RequestHeaderExample が表示されます。
6. HTTPヘッダにログイン済みユーザーのIDトークンのclaim値が連携されているのが分かります。
![httpd10.png](https://qiita-image-store.s3.amazonaws.com/0/209129/191eac87-61e0-6e60-a65a-d2c6a743fdb8.png)


## 認証が不要なURL
1. 認証が不要な http://kc-httpd.example.com/examples/jsp/ にアクセスします。
2. 認証していなくてもページを参照できるのが分かります。
![httpd11.png](https://qiita-image-store.s3.amazonaws.com/0/209129/7344fe72-32f6-e751-5071-449d9fd033bc.png)



## claimの値(role)によるアクセス制御
1. `admin`ロールが必要な http://kc-httpd.example.com/examples/websocket/echo.xhtml にアクセスします。
2. Keycloakに未ログインであるため、ログイン画面にリダイレクトが発生します。
3. `admin001` でログインします。
1. http://kc-httpd.example.com/examples/websocket/echo.xhtml が表示されます。(`admin` ロールを持たない`user001`でログインした場合は、 401エラーになります)
![httpd12.png](https://qiita-image-store.s3.amazonaws.com/0/209129/9b0209d5-821c-e45a-6198-bcbd0a7a3c6b.png)

:information_source: /examples/websocket/echo.xhtml のような静的ページへのアクセスの場合はブラウザにキャッシュが残るため、2回目以降のアクセスではログインしていなくてもブラウザのキャッシュでページが表示されてしまうことがあります。その場合は、<kbd>Ctrl</kbd>+<kbd>F5</kbd>でページを再読み込みすれば、ログイン画面が表示されます。

# まとめ

KeycloakとApacheをOpenID ConnectでID連携させる方法が確認できました。

今回は、mod_auth_openidcが標準パッケージで提供されているCentOS 7を使ったので、導入に関してほとんど手間がかかりませんでした。OS標準のパッケージの場合、バージョン1系で多少古いですが、OSベンダーがサポートしてくれるというメリットがあります。
mod_auth_openidcのサイトでは、EL6/EL7向けのRPM形式および、Debian/Ubuntuのdeb形式、Windows用が配布されているため、これらの環境でも容易にインストールが可能です。パッケージが提供されいない環境ではソースからのビルドが必要になります。

IDの連携部分に関しては、ユーザーIDをHTTPヘッダから取得する必要があるので、アプリケーションによってはコードに手を入れる必要があるかもしれません。

claimに含まれている値であれば、どの値でも取得が可能なので、
これを利用して、任意のclaim値によるアクセス制御が可能なので、Security Proxyに比べ、より細かなアクセス制御が可能です。
また、Security Proxyとは異なり、任意のclaim値をバックエンドに連携させることが可能です。

mod_auth_openidcはApache上のモジュールなので、リバースプロキシのアクセスログが記録できることや、OIDCで連携したログインIDをアクセスログを簡単に組み込むことがにできる点も大きなメリットです。

# 参考資料
- [Keycloak Documentation](http://www.keycloak.org/documentation.html)
 - [2.4.4. mod_auth_openidc Apache HTTPD Module](http://www.keycloak.org/docs/latest/securing_apps/index.html#_mod_auth_openidc) 
- [NRI OpenStandia Keycloak日本語ドキュメント](http://keycloak-documentation.openstandia.jp/master/ja_JP/securing_apps/index.html#_mod_auth_openidc)
- [mod_auth_openidc](https://github.com/pingidentity/mod_auth_openidc/)
  - [mod_auth_openidcの設定項目一覧](https://github.com/pingidentity/mod_auth_openidc/blob/master/auth_openidc.conf)
  - [Authorization](https://github.com/pingidentity/mod_auth_openidc/wiki/Authorization)