WebアプリケーションにKeycloakで簡単に認証認可を実装する

## はじめに
みなさん、認証認可ってちゃんと真面目に作ってますか？作ってるとは思うんですが、アプリ作る側からすると、認証認可ってめちゃくちゃメンドクサイですよね。この記事では、Traefik、Keycloak Gatekeeper、Keycloak、Nuxt.jsを用いて、Nuxt.jsで作成したアプリケーションに、簡単に認証認可機能を付与する方法およびそのDocker Composeを解説します。<font color=red>Gatekeeperは今はLouketo Proxyにプロジェクトを移管しているようですが</font>、Docker HubにLouketo Proxyの正式なイメージがなさそうなので、Gatekeeperを利用しています。個人が作成しているLouketo Proxyイメージを利用しても、動作は同じだと思います（未検証）。

### 構成
今回検証用に用いた環境は以下です。本当はNode側でKeycloakのアダプタを実装したかったのですが、アダプタの実装が予想外に面倒だったのと、Gatekeeperだと（開発）言語依存しないので、Gatekeeperを選択しました。なぜTraefikを前段に置いているかというと、まずポートを本番に近い形80または443のみにしたかったというのと、keycloakのAdminコンソールにログインできないようにしたかったためです（今回そこまでやってません）
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/816568b9-1af4-4eb3-a830-d6a6f54a8421.png" width="800px">

#### Docker Compose 構成
フォルダ構成は次の通り（app配下は省略）
詳細は下記参照
https://github.com/MichitoIchimaru/traefik_keycloak-gatekeeper_keycloak

```
Folder
│  docker-compose.yml
├─ docker
│  └─ app
│      Dockerfile
└─ volumes
   ├─gatekeeper
   │  config.yml
   ├─keycloak
   │  realm-app.json
   └─traefik
      traefik.yml
```

### Docker Compose

#### docker-compose.yml
``` docker-compose.yml
version: "3"

services:
  reverse-proxy:
    image: traefik:latest
    ports:
      - 80:80
      - 8080:8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./volumes/traefik/traefik.yml:/etc/traefik/traefik.yml

  keycloak-gatekeeper:
    container_name: keycloak-gatekeeper
    image: keycloak/keycloak-gatekeeper
    command: ['--config', '/opt/config.yml']
    volumes:
      - ./volumes/gatekeeper/config.yml:/opt/config.yml
    expose:
      - 3000
    restart: always
    depends_on:
      - keycloak
      - app
    labels:
      - traefik.enable=true
      - traefik.http.routers.gk.rule=Host(`gk.192.168.56.103.nip.io`)

  keycloak:
    container_name: keycloak
    image: jboss/keycloak:latest
    expose:
      - 8080
    volumes:
      - ./volumes/keycloak/realm-app.json:/opt/jboss/import/realm-app.json
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloak
      DB_USER: keycloak
      DB_SCHEMA: public
      DB_PASSWORD: password
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: password
      KEYCLOAK_FRONTEND_URL: http://keycloak.192.168.56.103.nip.io/auth
      KEYCLOAK_IMPORT: /opt/jboss/import/realm-app.json
    depends_on:
      - postgres
      - app
    labels:
      - traefik.enable=true
      - traefik.http.routers.kc.rule=Host(`keycloak.192.168.56.103.nip.io`)

  postgres:
    image: postgres
    volumes:
      - /var/postgres/data:/var/lib/postgresql/data
    expose:
      - 5432
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password

  app:
    container_name: app
    build: docker/app
    expose:
      - 3000
```
次のtraefik用のラベルでルーティングするホスト指定しています。

```
      - traefik.http.routers.gk.rule=Host(`gk.192.168.56.103.nip.io`)
      - traefik.http.routers.kc.rule=Host(`keycloak.192.168.56.103.nip.io`)
```
http://gk.192.168.56.103.nip.io -> gatekeeper:3000
http://keycloak.192.168.56.103.nip.io -> keycloak:8080
nip.io（フリーのワイルドカードDNS）については以下を参照
[フリーなワイルドカードDNSサービス](https://qiita.com/charon/items/e1d4dd9c3d6e5af1e106)
ラベルでホスト指定しているので、traefik.ymlの中のデフォルトルールは不要ではあります。
nip.ioを利用できない環境下の場合は、その環境のDNSに登録するか、hostsでIPを指定しても動きます。
TraefikでPathによるルーティングを色々と試してはみたんですが、Keycloak、Nuxt.jsは問題なくできるものの、Gatekeeperのコンテキストルートを変更する手段が見当たらず、断念しました。
また、Traefikの8080は開ける必要はないですが、ダッシュボード確認のために開けています。

```
      - /var/postgres/data:/var/lib/postgresql/data
```
私の環境がVirtualBoxの共有フォルダをdockerの実行場所としているので、PostgreSQLを起動するときに共有フォルダを指定してもそこにはデータは作成されないので、上記のような指定にしています。環境に合わせて修正してください。ymlの中でvolume作成した方が素直かもです。keycloakの設定を変えたい場合はここをごっそり消して docker-compose up してください。

#### traefik/traefik.yml
``` traefik.yml
api:
  dashboard: true
  insecure: true

providers:
  docker:
    exposedByDefault: false
    defaultRule: "Host(`{{ index .Labels \"traefik.host\" }}.192.168.56.103.nip.io`)"

entryPoints:
  http:
    address: ":80"
```
ここの説明は特に不要かと思います。前述した通り、docker-compose.yml側のlabelでルーティングのルールを指定しているので、ここのdefaultRuleは使われません。
exposedByDefaultを<font color=#ff3333>false</font>に設定しているので、docker-compose.yml側で<font color=#ff3333>traefik.enable=true</font>ラベルが付いてないとtraefikに反映されません。

#### gatekeeper/config.yml
``` config.yml
discovery-url: http://keycloak.192.168.56.103.nip.io/auth/realms/app

client-id: gatekeeper-api
client-secret: '123456'

listen: 0.0.0.0:3000

redirection-url: http://gk.192.168.56.103.nip.io
encryption-key: 1234567890123456
enable-session-cookies: true
enable-logout-redirect: true
secure-cookie: false

upstream-url: http://app:3000

upstream-keepalives: true
skip-upstream-tls-verify: true
scopes: []
enable-security-filter: true
resources:
  - uri: /
    white-listed: true
  - uri: /_nuxt/*
    white-listed: true
  - uri: /_loading/*
    white-listed: true
  - uri: /__webpack_hmr/*
    white-listed: true
  - uri: /img/*
    white-listed: true
  - uri: /favicon.ico
    white-listed: true
  - uri: /user
    roles:
      - user
  - uri: /user/*
    roles:
      - user
  - uri: /admin
    roles:
      - admin
  - uri: /admin/*
    roles:
      - admin
```
この設定および下に記載するkeycloakのrealm-app.jsonについては、ここを参考にしました。
http://lab.astamuse.co.jp/entry/keycloak/kc_gatekeeper_and_wordpress
https://github.com/yoshixmk/kc-gatekeeper-example
<b><font color=red>ただし、この記事については反面教師にもさせていただきました。</font></b>
元記事ではredirection-urlとupstream-urlが同じになっています。しかもリダイレクト先が本来見えてはいけないアプリ側になっています。これだとgatekeeperを入れている意味がありません。redirection-urlはkeycloakのリダイレクト先、upstream-urlはgatekeeperの後ろでサービスを起動しているアプリのURLになります。手動設定については、こちらが参考になります。
http://c.itdo.jp/technical-information/idmanagement/keycloak-gatekeeper-minimal/
https://qiita.com/k2n/items/635e0b08ecac421c56d2

resources: で認証をかけるページ（Path）を指定しています。上記設定では、keycloakのuserロールに対して/user配下、adminロールに対して/admin配下にアクセスできるようにしています。/_nuxt/*などのNuxt.jsに必要なPathについては無条件にアクセスできるようにしています。

#### keycloak/realm-app.json
realm-app.jsonは長いので重要なところだけ記載します。

``` realm-app.json
  "roles": {
    "realm": [
      {
        "id": "b1e95d4c-aff4-4856-8231-80a4ca0785a3",
        "name": "admin",
        "composite": true,
        "composites": {
          "realm": [
            "user"
          ]
        },
        "clientRole": false,
        "containerId": "app",
        "attributes": {}
      },
```
この部分でロールを定義しています。keycloakのresources定義で複数のロールを指定できなかった（方法はあるかもしれません）ので、adminロールはuserロールも兼ねるように、compositesでuserを指定しています。

``` realm-app.json
    {
      "id": "f280a3d6-bdc2-4293-9c15-5b915f9467e1",
      "clientId": "gatekeeper-api",
      "rootUrl": "http://gk.192.168.56.103.nip.io",
      "adminUrl": "",
      "surrogateAuthRequired": false,
      "enabled": true,
      "alwaysDisplayInConsole": false,
      "clientAuthenticatorType": "client-secret",
      "secret": "123456",
      "redirectUris": [
        "http://gk.192.168.56.103.nip.io/*"
      ],
      "webOrigins": [
        "http://gk.192.168.56.103.nip.io"
      ],
```
この部分に関しては環境に合わせて変更してください。

``` realm-app.json
  "users": [
    {
      "username": "admin",
      "enabled": true,
      "totp": false,
      "emailVerified": false,
      "firstName": "admin",
      "lastName": "admin",
      "email": "admin@example.com",
      "credentials": [
        {
          "temporary": false,
          "type": "password",
          "value": "password"
        }
      ],
      "realmRoles": [ "offline_access", "uma_authorization", "admin" ]
    },
    {
      "username": "user01",
      "enabled": true,
      "totp": false,
      "emailVerified": false,
      "firstName": "01",
      "lastName": "user",
      "email": "user01@example.com",
      "credentials": [
        {
          "temporary": false,
          "type": "password",
          "value": "password"
        }
      ],
      "realmRoles": [ "offline_access", "uma_authorization", "user" ]
    },
```
ここでユーザーを定義しています。ここに関してはKeycloakの画面で作成したり、LDAP連携した方がいいかと思いますが、動作検証するためのdockerとして作っているので、docker-compose up するだけで全てがセットアップされるようにしています。KeycloakはUser情報を画面からエクスポートできず、CLIでのエクスポートがあるにはあるんですが、うまく動かなかったので、この部分のフォーマットを調査するのがだいぶ難儀でした...。
以下のユーザーを作成するようにしています。このadminはkeycloakのadminとは別アカウントです。appレルムに対するadminユーザーになります。

| ID | Password |
|:--------|:----------|
| admin   | password  |
| user01  | password  |
| user02  | password  |
| user03  | password  |
| user04  | password  |
| user05  | password  |

### 起動 & 動作確認
docker-compose upで起動してください。

#### Traefik
http://192.168.56.103.nip.io:8080 にアクセスするとTraefikのダッシュボードが見れるはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/d21cde6a-1f9b-5111-a186-89b0acc6836e.png" width="800px">

#### Keycloak
http://keycloak.192.168.56.103.nip.io にアクセスすると、Keycloakの管理コンソールが見れるはずです。ID/PWはdocker-compose.ymlに記載しているadmin/passwordです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/1b4e613f-ad29-44f6-6d72-fb3f500e6747.png" width="800px">

#### Nuxt.jsアプリ
http://gk.192.168.56.103.nip.io にアクセスすると、Gatekeeperの後ろで動いているNuxt.jsのアプリにアクセスできるはずです。
![app01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/2ad08b7a-7a7c-f0ab-c655-fd9211bfb187.png)

アプリ側でのログイン処理は/userに遷移しているだけです。

``` layouts/default.vue
        <v-btn
          color="green"
          dark
          href="/user"
        >
          LOGIN
        </v-btn>
```

LOGINボタンを押してみましょう。keycloakに遷移してID/PWを求められるはずです。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/4153fc8c-e1e3-fa28-dcbe-57449ba63a52.png" width="800px">
user01/password でログインしてみてください。するとユーザー画面が表示されるはずです。
![app03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/70b0deb5-e8f3-9ec9-cee3-964c54cf0ff2.png)
この状態でURLを/userから/adminに変更して管理者ページを表示しようとしてみてください。アクセスが拒否されるはずです。
![app04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/958211aa-c3ed-1edb-bb28-4f41a82538af.png)
/userに戻ってログアウト後、今度は admin/password でログインしてみてください。
adminロールにはcompositesでuserロールも付与しているので、Userページが表示されるはずです。/adminを表示しようとすると、今度は正常に表示されるはずです。
![app05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/8d75c035-d5d4-d72f-54a2-3c1e8fa4d9c0.png)
ここでログアウトについて触れます。いろんな記事を読みましたが、Gatekeeperを用いた場合のログアウト処理を記載している記事はまったくありませんでした。みなロールによるページ振り分けを検証しただけで終わっています。
ということでやってみました。最初はKeycloakのログアウトエンドポイントをredirect_uri付けて呼び出せばいいんだろうなぁと思ってたんですが、それだとうまくログアウトしてくれませんでした。

```
http://keycloak.192.168.56.103.nip.io/auth/realms/app/protocol/openid-connect/logout?redirect_uri=http%3A%2F%2Fgk.192.168.56.103.nip.io
```

Keycloakのログアウトエンドポイントは問題なく遷移できてredirect_uriにもリダイレクトしてくれるんですが、LOGINボタンを押すと認証を聞かれずにuserページが表示されます。つまりログアウトされてない...。
いろいろと調べたところ、答えは簡単でした。Gatekeeper側のログアウトエンドポイントを呼び出さないといけないようです。ここに書いてありました。何事もちゃんとドキュメント読めってことですね...。
https://github.com/louketo/louketo-proxy/blob/master/docs/user-guide.md

``` user-guide.md
A /oauth/logout?redirect=url is provided as a helper to log users out.
In addition to dropping any session cookies, we also attempt to revoke access 
via revocation URL (config revocation-url or --revocation-url) with the provider. 
For Keycloak, the URL for this would be https://keycloak.example.com/auth/realms/REALM_NAME/protocol/openid-connect/logout. 
If the URL is not specified we will attempt to grab the URL from the OpenID discovery response.
```
Gatekeeperの/oauth/logoutを呼び出せばよさそうです。redirectは付与していなければ、keycloakから取得します。

``` layouts/app.vue
        <v-btn
          color="green"
          dark
          href="/oauth/logout"
        >
          LOGOUT
        </v-btn>
```

次に、ユーザー情報の取得です。ログインした状態で次のURLにアクセスします。すると、ログインユーザーの情報を返します。
http://gk.192.168.56.103.nip.io/api/v1/users/me
![api_res.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82651/564bb067-119f-b6ea-e829-7e85235c1710.png)
サーバ側のコードは次の通りです。x-auth-****ヘッダーでログインユーザーの情報を取得できます。
サーバサイドはKoa.jsで実装しています。

``` routes/users.js
const Router = require('koa-router')
const router = new Router()
const logger = require('../utils/logger')

// /v1/users#GET
router.get('/me', async (ctx) => {
  ctx.status = 200
  ctx.body = {
    uid: ctx.headers['x-auth-userid'],
    name: ctx.headers['x-auth-username'],
    mail: ctx.headers['x-auth-email'],
    role: ctx.headers['x-auth-roles']
  }
})

module.exports = router
```

Cookieのkc-accessにJWTが格納されていますが、改ざんされた場合、ちゃんとGatekeeperがKeycloakと連携してログインしてないよとみなしてくれます。

### 背景
みなさん、WebアプリケーションをPoCするときって、何を考えます？
まずは「どのフレームワーク（言語）使おうかなー、DB何使おうかなー」って考えると思うんですよ。作るシステムに適したフレームワーク、DBを選択して、次に「ORMどうしようかなー、ORM使ってメリットあったことないしなー、てかグラフDB使いたいからORMとかないしなー」とか考えながら実装に入ると思います。まず最初に何の機能作ります？認証認可ですよね？でもこの認証認可がとても厄介者。そもそも 独自認証？ LDAP連携？ MFA？ SSO？ OAuth？ OpenID Connect？ など考えないといけない事が多すぎ。そして実装も厄介。 Salt？ Stretching？ JWT？ localStorage使うな？ もう認証認可考えるだけでおなか一杯。
こんな人たちとはあまり関わりたくない。ロジックに集中させて！！ということで、OSSでかつ、認証認可、SSOやMFAまでやってくれるKeycloakを触ってみたいなーと思い、その検証結果です。
Keycloakだと結構簡単に認証認可とアプリを切り離せそうです。ですがせめて管理コンソールはポートを分けてほしいところです。
<h3>そしてここまで色々調べて検証してみたけど、今や世の中サーバーレスだしなぁ...。</h3>