はじめに
みなさん、認証認可ってちゃんと真面目に作ってますか?作ってるとは思うんですが、アプリ作る側からすると、認証認可ってめちゃくちゃメンドクサイですよね。この記事では、Traefik、Keycloak Gatekeeper、Keycloak、Nuxt.jsを用いて、Nuxt.jsで作成したアプリケーションに、簡単に認証認可機能を付与する方法およびそのDocker Composeを解説します。Gatekeeperは今はLouketo Proxyにプロジェクトを移管しているようですが、Docker HubにLouketo Proxyの正式なイメージがなさそうなので、Gatekeeperを利用しています。個人が作成しているLouketo Proxyイメージを利用しても、動作は同じだと思います(未検証)。
構成
今回検証用に用いた環境は以下です。本当はNode側でKeycloakのアダプタを実装したかったのですが、アダプタの実装が予想外に面倒だったのと、Gatekeeperだと(開発)言語依存しないので、Gatekeeperを選択しました。なぜTraefikを前段に置いているかというと、まずポートを本番に近い形80または443のみにしたかったというのと、keycloakのAdminコンソールにログインできないようにしたかったためです(今回そこまでやってません)
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
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サービス
ラベルでホスト指定しているので、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
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をfalseに設定しているので、docker-compose.yml側でtraefik.enable=trueラベルが付いてないとtraefikに反映されません。
gatekeeper/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
ただし、この記事については反面教師にもさせていただきました。
元記事では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は長いので重要なところだけ記載します。
"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を指定しています。
{
"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"
],
この部分に関しては環境に合わせて変更してください。
"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のダッシュボードが見れるはずです。
Keycloak
http://keycloak.192.168.56.103.nip.io にアクセスすると、Keycloakの管理コンソールが見れるはずです。ID/PWはdocker-compose.ymlに記載しているadmin/passwordです。
Nuxt.jsアプリ
http://gk.192.168.56.103.nip.io にアクセスすると、Gatekeeperの後ろで動いているNuxt.jsのアプリにアクセスできるはずです。
アプリ側でのログイン処理は/userに遷移しているだけです。
<v-btn
color="green"
dark
href="/user"
>
LOGIN
</v-btn>
LOGINボタンを押してみましょう。keycloakに遷移してID/PWを求められるはずです。
user01/password でログインしてみてください。するとユーザー画面が表示されるはずです。
この状態でURLを/userから/adminに変更して管理者ページを表示しようとしてみてください。アクセスが拒否されるはずです。
/userに戻ってログアウト後、今度は admin/password でログインしてみてください。
adminロールにはcompositesでuserロールも付与しているので、Userページが表示されるはずです。/adminを表示しようとすると、今度は正常に表示されるはずです。
ここでログアウトについて触れます。いろんな記事を読みましたが、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
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から取得します。
<v-btn
color="green"
dark
href="/oauth/logout"
>
LOGOUT
</v-btn>
次に、ユーザー情報の取得です。ログインした状態で次のURLにアクセスします。すると、ログインユーザーの情報を返します。
http://gk.192.168.56.103.nip.io/api/v1/users/me
サーバ側のコードは次の通りです。x-auth-****ヘッダーでログインユーザーの情報を取得できます。
サーバサイドはKoa.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だと結構簡単に認証認可とアプリを切り離せそうです。ですがせめて管理コンソールはポートを分けてほしいところです。