Keycloakハンズオン


先日社内で開催した勉強会にてKeycloakのハンズオンを行いましたのでその時のテキストを公開します。

---

Dockerを使ってKeycloakで認証サーバーを建て、WEBサーバーに対してリバースプロキシ方式で認証をかけます。

## 1. Keycloak作成
早速Keycloakを使って認証サーバーを建てます。

### 1-1. ネットワーク作成
コンテナ間で通信を行うので、最初にコンテナが所属するネットワークを作成します。

```shell
$ docker network create --driver bridge keycloak
```

### 1-2. コンテナ作成
Keycloakのコンテナを建てます。Keycloakの開発元がコンテナイメージを提供してくれているので、それを使います。

```yaml:keycloak.yaml
version: "3"

services:
  keycloak:
    image: jboss/keycloak:12.0.2
    environment:
      KEYCLOAK_USER: administrator
      KEYCLOAK_PASSWORD: "00000000"
    ports:
    - 8080:8080
    networks:
    - keycloak

networks:
  keycloak:
    external: true
```

KEYCLOAK_USERとKEYCLOAK_PASSWORDは管理コンソールへのログインIDとパスワードです。

### 1-3. コンテナ起動

```shell
$ docker-compose -f keycloak.yaml up -d
```

### 1-4. 起動確認
ブラウザを開き、`http://localhost:8080`にアクセスするとウェルカムページが表示されます。

## 2. 認証サーバーの設定
Keycloakの設定をしていきます。

### 2-1. 管理コンソールへのログイン
1-4でアクセスしたウェルカムページの`Administration Console`をクリックしてください。
ログインページが表示されるので、`KEYCLOAK_USER`と`KEYCLOAK_PASSWORD`で設定したユーザーIDとパスワードを入力してログインしてください。
ログインに成功すると管理コンソールの画面へ遷移します。

### 2-2. 日本語化
ひとまず、分かりづらいかと思いますので、日本語化します。
右側にあるタブの`Themes`をクリックしてください。
`Internationalization Enabled`を`ON`にすると`Supported Locales`と`Default Locale`が表示されます。`Default Locale`を`ja`にして`Save`ボタンを押してください。
日本語に変わらない場合は、右上の`Administrator`(もしくは`KEYCLOAK_USER`で設定した名前)をクリックして、`Manage account`をクリックしてください。アカウント管理画面が日本語で表示されたら、`セキュリティ管理コンソールに戻る`で日本語化した管理画面に遷移します。

### 2-3. レルムの作成
現在表示されてるのは`Master`レルムの管理ページです。レルムというのはKeycloakのルートデータで、それぞれが独立した認証プロバイダとなります。`Master`レルムは全てのレルムへのアクセス権をデフォルトで持ってるレルムで、ここにログインすると全てのレルムの管理を行うことができます。
基本的に`Masterレルム`はKeycloakの管理を行う場所で、ここを認証プロバイダとして使うことはないので、新しいレルムを作りましょう。左のサイドメニューの上にある`Master`にマウスオーバーすると、`レルムの追加`というボタンが出てくるので押してください。レルムの追加画面に遷移するので、`名前`にレルム名を入力して`作成`を押すとレルムが作られます。今回は`handson`で作成します。

### 2-4. レルムの設定
作成すると、サイドメニューの上の`Master`が`Handson`(もしくは設定したレルム名)になってると思います。ここが`Handson`レルムの管理ページです。
右側の設定ページの下の方に`エンドポイント`とあります。そこの`OpenIDエンドポイントの設定`をクリックしてください。OpenID Connectに必要な各種設定値が記載されたJSONを取得できます。

```json
{
  "issuer": "http://localhost/auth/realms/handson",
  "authorization_endpoint": "http://localhost:8080/auth/realms/handson/protocol/openid-connect/auth",
  "token_endpoint": "http://localhost:8080/auth/realms/handson/protocol/openid-connect/token",,
  "userinfo_endpoint": "http://localhost:8080/auth/realms/handson/protocol/openid-connect/userinfo",
  ...
}
```

この設定値を取得するためのエンドポイントは`OpenID Connect`の仕様に定められており、`OpenID Connect`に準拠してる認証サービスにはこの`[ベースURL]/.well-known/openid-configuration`というエンドポイントは必ず存在します。例えば、Googleの場合はhttps://accounts.google.com/.well-known/openid-configuration にあります。ここに記述される属性は[こちら](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata)で定義されています。
この設定値のうち、`issuer`の値は後で使いますので控えておいてください。
では、いくつか基本的な設定をします。右側のタブの`ログイン`をクリックしてください。遷移後の画面で以下を`ON`にします。
- ユーザー登録
- パスワード忘れ
- ログイン状態の保存

以上の設定で、よくあるログインページを表示できるようになります。`ON`にしたら`保存`を押してください。

### 2-5. クライアント作成
認証するアプリ(RP)の情報を登録していきます。左側のサイドメニューから`クライアント`をクリックしてください。右側の画面に登録されてるクライアントの一覧が表示されたら、右上の`作成`を押してください。
`クライアントの追加`画面が表示されたら、`クライアントID`を入力して、`保存`を押してください。今回は`nginx`とします。

### 2-6. クライアント設定
クライアントが作成されるとクライアントの設定画面に遷移しますので、セキュアに認証するためにいくつか設定していきます。
まず、`アクセスタイプ`を`public`から`confidential`に変えてください。これで`クライアントシークレット`が発行されます。
次に、`有効なリダイレクトURI`を入力します。これは認証後にサービスに戻るURIなので、RPとなるサービスのURLになるかと思います。今回はローカルのWEBサーバーなので、`http://localhost/*`と入れてください。
入力したら一番下の`保存`を押してください。`保存`を押すと一番上のタブに`クレデンシャル`というものが出てきます。`クレデンシャル`タブに移動すると`シークレット`というものがあります。これは認証時に使用する`クライアントシークレット`となるので控えておいてください。

## 3. WEBサーバー
WEBサーバーを作成します。Nginxを使用します。

### 3-1. Nginxコンテナ作成
```yaml:client.yaml
version: "3"

services:
  nginx:
    image: nginx
    ports:
    - 80:80
    networks: 
    - keycloak

networks:
  keycloak:
    external: true
```

### 3-2. コンテナ起動
```shell
$ docker-compose -f client.yaml up -d
```

### 3-3. 起動確認
ブラウザを開き、 `http://localhost` にアクセスしてください。
Nginxのウェルカムページが表示されれば問題ありません。

## 4. 認証プロキシ
Nginxの代わりに認証プロセスを行ってくれるリバースプロキシを建てます。
公式が推奨してるオープンソースの[oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy)を使用します。

### 4-1. コンテナ作成
docker-composeの設定にプロキシコンテナを追加します。`[クライアントシークレット]`には先程クライアントの設定ページで控えたシークレットを入れてください。その他、レルム名やクライアント名を変えてる場合は適時置き換えてください。

```diff:client.yaml
version: "3"

services:
  nginx:
    image: nginx
    # 80番ポートはプロキシで使うのでnginxから削除
-   ports:
-   - 80:80
    networks: 
    - keycloak

+ proxy:
+   image: quay.io/oauth2-proxy/oauth2-proxy
+   environment:
+     OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
+     OAUTH2_PROXY_COOKIE_SECRET: 01234567890123456789012345678912
+     OAUTH2_PROXY_COOKIE_SECURE: "false"
+     OAUTH2_PROXY_COOKIE_NAME: "oauth2_proxy"
+     OAUTH2_PROXY_EMAIL_DOMAINS: "*"
+     OAUTH2_PROXY_UPSTREAMS: http://nginx/
+     OAUTH2_PROXY_PROVIDER: oidc
+     OAUTH2_PROXY_OIDC_ISSUER_URL: http://keycloak:8080/auth/realms/handson
+     OAUTH2_PROXY_CLIENT_ID: nginx
+     OAUTH2_PROXY_CLIENT_SECRET: [クライアントシークレット]
+     OAUTH2_PROXY_REDIRECT_URL: http://localhost/oauth2/callback
+     OAUTH2_PROXY_SCOPE: openid email profile
+     OAUTH2_PROXY_PASS_ACCESS_TOKEN: "true"
+     OAUTH2_PROXY_PASS_USER_HEADERS: "true"
+     OAUTH2_PROXY_PASS_AUTHORIZATION_HEADER: "true"
+     OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
+     OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
+   ports:
+   - 80:4180
+   networks: 
+   - keycloak

networks:
  keycloak:
    external: true
```

一個一個説明すると時間がかかるのでポイントだけ説明すると、`OAUTH2_PROXY_OIDC_ISSUER_URL`には先程の`OpenID Connect設定値エンドポイント`で控えた`issuer`のURLを入力します。ただし、このプロキシから見た`Keycloak`のURLにしてください。docker-composeはサービス名でルーティングできるので、`localhost`を`keycloak`に変更します。認証時にリダイレクトされるURLも`keycloak`になるので、以下のコマンドでホスト名を追加しておきます。

```shell
$ echo 127.0.0.1 keycloak >> /etc/hosts
```

`OAUTH2_PROXY_SCOPE`は認証後にRPに与えてほしい操作権限です。`openid`は`OpenID Connect`による認証処理の許可、`email`や`profile`はユーザーデータの取得許可です。
`OAUTH2_PROXY_UPSTREAMS`には認証後にリクエストを流すサーバーを指定します。今回は`Nginx`に渡します。
`PASS~`の設定は認証後のデータをUpstreamに渡すかどうかのフラグです。
`OAUTH2_PROXY_REDIRECT_URL`には認証後にリダイレクトしてほしいURLを指定します。`oauth2-proxy`の標準のコールバックエンドポイントは`/oauth2/callback`となります。

### 3-2. コンテナ起動
コンテナを起動します。先程起動したNginxが生きてる場合は、ポートが使われてるので、停止してから起動します。

```shell
$ docker-compose -f client.yaml down
$ docker-compose -f client.yaml up -d
```

### 3-3. 動作確認
ブラウザを開き、 `http://localhost` にアクセスしてください。
Keycloakのログインページへリダイレクトされます。ユーザーは登録してないので、下の`登録`からユーザー登録をしてください。`ユーザー名`は半角英数字にしてください。登録後、入力した`ユーザー名`と`パスワード`でログインしてください。ログインが成功したら`Nginx`のウェルカムページに飛びます。

## 4. WEBサーバーに飛んでるリクエストの中身を確認
実際に使う時に`oauth2-proxy`がどのように認証後のデータを投げてくるのか知ってないと、その後の処理ができないので、リクエストの中身を確認してみましょう。[httpbin](https://github.com/postmanlabs/httpbin)というアプリケーションを使います。

### 4-1. コンテナ作成

```diff:client.yaml
version: "3"

services:
  nginx:
    image: nginx
    networks: 
    - keycloak

+ httpbin:
+   image: kennethreitz/httpbin
+   networks: 
+   - keycloak

  proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy
    environment:
      ...
    ports:
    - 80:4180
    networks: 
    - keycloak

networks:
  keycloak:
    external: true
```

### 4-2. Nginxの設定
`Nginx`がリクエストを`httpbin`に流すように設定します。  
以下でNginxの設定ファイルを作成します。

```conf:nginx/conf.d/www.conf
server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://httpbin/;
    }
}
```

設定した作成ファイルを`Nginx`のコンテナにマウントします。また、`httpbin`が起動する前に`Nginx`が起動するのは具合が悪いので依存関係を設定します。

```diff:client.yaml
version: "3"

services:
  nginx:
    image: nginx
+   volumes: 
+   - ./nginx/conf.d:/etc/nginx/conf.d
+   depends_on:
+   - httpbin
    networks: 
    - keycloak

  httpbin:
    image: kennethreitz/httpbin
    networks: 
    - keycloak

  proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy
    environment:
      ...
    ports:
    - 80:4180
    networks: 
    - keycloak

networks:
  keycloak:
    external: true
```

### 4-2. コンテナ起動
コンテナを起動します。

```shell
$ docker-compose -f client.yaml up -d
```

### 4-3. 動作確認
ブラウザを開き、 `http://localhost/get` にアクセスしてください。以下のようなJSONが取得できるはずです。

```json
{
  "args": {},
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "ja,en-US;q=0.7,en;q=0.3",
    "Authorization": "Bearer xxx",
    "Cache-Control": "no-cache",
    "Connection": "close",
    "Cookie": "...",
    "Host": "httpbin",
    "Pragma": "no-cache",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "...",
    "X-Forwarded-Access-Token": "xxx",
    "X-Forwarded-Email": "foo@example.com",
    "X-Forwarded-Preferred-Username": "account_name",
    "X-Forwarded-User": "00000000-0000-0000-0000-000000000000"
  },
  "origin": "0.0.0.0",
  "url": "http://httpbin/get"
}
```

`httpbin`に飛んできたリクエストの中身がJSONで返ってきます。`oauth2-proxy`によって追加されたヘッダーは以下の通りです。
- Authorization
- X-Forwarded-Access-Token
- X-Forwarded-Email
- X-Forwarded-Preferred-Username
- X-Forwarded-User

`Preferred-Username`と`User`の違いは、`Preferred-Username`がログイン認証等にも使われる"ユーザーが設定したユーザーID"で、`User`が"認証サービス側が自動的に割り当てているユーザーID"です。

## 後始末
```shell
$ docker-compose -f keycloak.yaml -f client.yaml down
```