CLOVER🍀

That was when it all began.

Keycloak Gatekeeperを試す

これは、なにをしたくて書いたもの?

  • Keycloak Gatekeeperというものが、Keycloak 4.6.0.Finalで追加されたらしく
  • こちらを試してみようと

というわけで、Keycloak Gatekeeperを試してみようというエントリです。

Keycloak Gatekeeper

Keycloak 4.6.0のリリースノートを見てみます。

Keycloak Gatekeeper provides a security proxy that can be used to secure applications and services without an adapter. It can be installed locally alongside your application or as a sidecar on OpenShift or Kubernetes.

Keycloak 4.6.0.Final

アプリケーションの前にプロキシとして配置され、アプリケーションを保護する役割として配置されるようです。
OpenShiftやKubernetesとも一緒に使えるようですね。

ドキュメントは、こちら。

Keycloak Gatekeeper

Keycloak Gatekeeper自身は、Golangで実装されているようですね。

GitHub - keycloak/keycloak-gatekeeper: A OpenID / Keycloak Proxy service

お題

Keycloak Gatekeeperの背後にサンプルアプリケーションを配置して、OpenID Connectによる認証で保護してみます。

以下の内容を実現してみます。

  • ユーザーを2つ用意し、それぞれ異なるロール(user、admin)を割り当てる
  • サンプルアプリケーションには、以下のようなエンドポイントを用意する
    • ログインなしでもアクセスできる
    • ログインしていればアクセスできる
    • ログインしており、かつ特定のロールがないとアクセスできない

今回利用するKeycloakおよびKeycloak Gatekeeperのバージョンは、4.8.3.Finalとし、以下のような構成とします。

  • サンプルアプリケーション … 192.168.0.2:5000
  • Keycloak Gatekeeper … 172.17.0.2:3000
  • Keycloak … 172.17.0.3:8080

サンプルアプリケーション

それでは、最初にサンプルアプリケーションを作成してみます。

環境確認と、アプリケーション作成に利用するExpressをインストール。

$ node -v
v10.15.1

$ npm -v
6.4.1

$ npm i express

Expressのバージョンは、こちらです。

  "dependencies": {
    "express": "^4.16.4"
  }

アプリケーションコード。
server.js

const express = require("express");
const app = express();

app.set("json spaces", 2);

const handleRequest = (message, req, res) => {
    console.log(`[${new Date()}] access = ${req.path}`);

    const headers = req.headers;

    const response = { path: req.path, message, headers };

    res.send(response);
}

// non-login
app.get("/public/index", (req, res) => handleRequest("Hello Public Area!!", req, res));

// secure
app.get("/secure/all", (req, res) => handleRequest("Hello Secure Area!!", req, res));
app.get("/secure/user", (req, res) => handleRequest("Hello Secure User Area!!", req, res));
app.get("/secure/admin", (req, res) => handleRequest("Hello Secure Admin Area!!", req, res));

console.log(`[${new Date()}] server startup`);

app.listen(5000);

アプリケーション上は、特にログイン有無やロールの確認をしていません。

また、受け取ったHTTPヘッダーをレスポンスに含めるようにしています。

あとは、package.jsonで起動できるように設定して

  "scripts": {
    "run": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

起動。

$ npm start

> ...
> node server.js

[Sat Feb 23 2019 18:48:53 GMT+0900 (GMT+09:00)] server startup

これで、準備は完了です。

Keycloak側の設定

Keycloakのダウンロードおよび展開、起動後、Keycloakの管理ユーザーを作成します。

$ bin/add-user-keycloak.sh -u keycloak-admin -p password
$ bin/jboss-cli.sh -c reload

Keycloakの再起動が完了したら、作成したユーザーで管理コンソールからログインします。

以下のRealmを作成。

  • Realm … demo-api

Clientsより、Clientを追加します。

この時、「Mappers」を選び、Protocol Mapperを追加する必要があります。

  • Mapper Type … Audience
  • Included Client Audience … 作成したClientの名前(今回は「sample-gatekeeper-api」)

この手順が必要なのは、以下が理由です。

There is a known issue with the Keycloak server 4.7.0.Final in which Gatekeeper is unable to find the client_id in the aud claim. This is due to the fact the client_id is not in the audience anymore. The workaround is to add the "Audience" protocol mapper to the client with the audience pointed to the client_id.

Known Issues

作成したProtocol Mapperは、こちら。

f:id:Kazuhira:20190223190824p:plain

では、続きを。

Rolesより、「user」と「admin」ロールを作成。

Usersからユーザーを2つ作成します。元からあるロールは、今回はそのままにしました。

  • user001 / Role userを追加
  • admin001 / Role adminを追加

Keycloak側の準備は、ここまでです。

Keycloak Gatekeeper

では、Keycloak Gatekeeper側を用意しましょう。

ダウンロードと展開。

$ wget https://downloads.jboss.org/keycloak/4.8.3.Final/gatekeeper/keycloak-gatekeeper-linux-amd64.tar.gz
$ tar xf keycloak-gatekeeper-linux-amd64.tar.gz

展開される内容はいたってシンプルで、「keycloak-gatekeeper」というファイルひとつが出てきます。

Keycloak Gatekeeperは、起動引数でYAMLで書いた設定ファイルを指定するか、設定そのものを起動引数で与えるかで
設定を行います。

Configuration options

Example usage and configuration

今回は、YAMLファイルを用意しましょう。

gatekeeper.yml

client-id: sample-gatekeeper-api
client-secret: fe557981-f9cf-4d46-8dff-b5ab37d3332a
discovery-url: http://172.17.0.3:8080/auth/realms/demo-api
listen: 0.0.0.0:3000
enable-refresh-tokens: true
secure-cookie: false
redirection-url: http://172.17.0.2:3000
encryption-key: 3lKvZzSmAq8DoMYGPRkVfSQrF27GKCPF
upstream-url: http://192.168.0.2:5000
resources:
  - uri: /public/*
    white-listed: true
  - uri: /secure/all
    methods:
      - GET
  - uri: /secure/user
    methods:
      - GET
    roles:
      - user
  - uri: /secure/admin
    methods:
      - GET
    roles:
      - admin

設定内容は、以下の通りです。

  • client-id、client-secret … 作成したClientのもの
  • discovery-url … KeycloakのRealmに対するエンドポイント
  • listen … Keycloak Gatekeeperのリッスンアドレスおよびポート
  • enable-refresh-tokens … Refresh Tokenのハンドリングを有効にするか(デフォルトfalse)
  • secure-cookie … バックエンドがhttpsではない場合、falseにする(デフォルトtrue)
  • redirection-url … OAuthのコールバックURLを指定。Keycloak Gatekeeper(自分自身)に向ければOK
  • encryption-key … セッションの暗号化キー。今回は、適当に作成
  • upstream-url … バックエンドのURL

resources配下が、各リソースに対するアクセス設定です。今回は、以下のように設定しました。

  • /public/* 配下 … 誰でもアクセス可(white-listed: true)
  • /secure/all … ログインしていればアクセス可
  • /secure/user … userロールを持っていればアクセス可
  • /secure/admin … adminロールを持っていればアクセス可

Keycloak Gatekeeperでは、定義されていないリソースパスは全部要ログインな感じで弾かれるので、必要なパスは
きちんと定義しておきましょうね。

なお、設定項目は「--help」で確認できます。

$ ./keycloak-gatekeeper --help
NAME:
   keycloak-gatekeeper - is a proxy using the keycloak service for auth and authorization

USAGE:
   keycloak-gatekeeper [options]

VERSION:
   4.8.3 (git+sha: e5bd7e6-dirty, built: 15-01-2019)

AUTHOR:
   Keycloak <keycloak-user@lists.jboss.org>

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --config value                            path the a configuration file [$PROXY_CONFIG_FILE]
   --listen value                            the interface the service should be listening on [$PROXY_LISTEN]
   --listen-http value                       interface we should be listening [$PROXY_LISTEN_HTTP]
   --discovery-url value                     discovery url to retrieve the openid configuration [$PROXY_DISCOVERY_URL]
   --client-id value                         client id used to authenticate to the oauth service [$PROXY_CLIENT_ID]
   --client-secret value                     client secret used to authenticate to the oauth service [$PROXY_CLIENT_SECRET]
   --redirection-url value                   redirection url for the oauth callback url, defaults to host header is absent [$PROXY_REDIRECTION_URL]
   --revocation-url value                    url for the revocation endpoint to revoke refresh token [$PROXY_REVOCATION_URL]
   --skip-openid-provider-tls-verify         skip the verification of any TLS communication with the openid provider (default: false)
   --openid-provider-proxy value             proxy for communication with the openid provider
   --openid-provider-timeout value           timeout for openid configuration on .well-known/openid-configuration (default: 30s)
   --base-uri value                          common prefix for all URIs [$PROXY_BASE_URI]
   --oauth-uri value                         the uri for proxy oauth endpoints (default: "/oauth") [$PROXY_OAUTH_URI]
   --scopes value                            list of scopes requested when authenticating the user
   --upstream-url value                      url for the upstream endpoint you wish to proxy [$PROXY_UPSTREAM_URL]
   --upstream-ca value                       the path to a file container a CA certificate to validate the upstream tls endpoint
   --resources value                         list of resources 'uri=/admin*|methods=GET,PUT|roles=role1,role2'
   --headers value                           custom headers to the upstream request, key=value
   --preserve-host                           preserve the host header of the proxied request in the upstream request (default: false)
   --request-id-header value                 the http header name for request id (default: "X-Request-ID") [$PROXY_REQUEST_ID_HEADER]
   --response-headers value                  custom headers to added to the http response key=value
   --enable-self-signed-tls                  create self signed certificates for the proxy (default: false) [$PROXY_ENABLE_SELF_SIGNED_TLS]
   --self-signed-tls-hostnames value         a list of hostnames to place on the self-signed certificate
   --self-signed-tls-expiration value        the expiration of the certificate before rotation (default: 3h0m0s)
   --enable-request-id                       indicates we should add a request id if none found (default: false) [$PROXY_ENABLE_REQUEST_ID]
   --enable-logout-redirect                  indicates we should redirect to the identity provider for logging out (default: false)
   --enable-default-deny                     enables a default denial on all requests, you have to explicitly say what is permitted (recommended) (default: true)
   --enable-encrypted-token                  enable encryption for the access tokens (default: false)
   --enable-logging                          enable http logging of the requests (default: false)
   --enable-json-logging                     switch on json logging rather than text (default: false)
   --enable-forwarding                       enables the forwarding proxy mode, signing outbound request (default: false)
   --enable-security-filter                  enables the security filter handler (default: false) [$PROXY_ENABLE_SECURITY_FILTER]
   --enable-refresh-tokens                   enables the handling of the refresh tokens (default: false) [$PROXY_ENABLE_REFRESH_TOKEN]
   --enable-session-cookies                  access and refresh tokens are session only i.e. removed browser close (default: true)
   --enable-login-handler                    enables the handling of the refresh tokens (default: false) [$PROXY_ENABLE_LOGIN_HANDLER]
   --enable-token-header                     enables the token authentication header X-Auth-Token to upstream (default: true)
   --enable-authorization-header             adds the authorization header to the proxy request (default: true) [$PROXY_ENABLE_AUTHORIZATION_HEADER]
   --enable-authorization-cookies            adds the authorization cookies to the uptream proxy request (default: true) [$PROXY_ENABLE_AUTHORIZATION_COOKIES]
   --enable-https-redirection                enable the http to https redirection on the http service (default: false)
   --enable-profiling                        switching on the golang profiling via pprof on /debug/pprof, /debug/pprof/heap etc (default: false)
   --enable-metrics                          enable the prometheus metrics collector on /oauth/metrics (default: false)
   --filter-browser-xss                      enable the adds the X-XSS-Protection header with mode=block (default: false)
   --filter-content-nosniff                  adds the X-Content-Type-Options header with the value nosniff (default: false)
   --filter-frame-deny                       enable to the frame deny header (default: false)
   --content-security-policy value           specify the content security policy
   --localhost-metrics                       enforces the metrics page can only been requested from 127.0.0.1 (default: false)
   --access-token-duration value             fallback cookie duration for the access token when using refresh tokens (default: 720h0m0s)
   --cookie-domain value                     domain the access cookie is available to, defaults host header
   --cookie-access-name value                name of the cookie use to hold the access token (default: "kc-access")
   --cookie-refresh-name value               name of the cookie used to hold the encrypted refresh token (default: "kc-state")
   --secure-cookie                           enforces the cookie to be secure (default: true)
   --http-only-cookie                        enforces the cookie is in http only mode (default: true)
   --match-claims value                      keypair values for matching access token claims e.g. aud=myapp, iss=http://example.*
   --add-claims value                        extra claims from the token and inject into headers, e.g given_name -> X-Auth-Given-Name
   --tls-cert value                          path to ths TLS certificate
   --tls-private-key value                   path to the private key for TLS
   --tls-ca-certificate value                path to the ca certificate used for signing requests
   --tls-ca-key value                        path the ca private key, used by the forward signing proxy
   --tls-client-certificate value            path to the client certificate for outbound connections in reverse and forwarding proxy modes
   --skip-upstream-tls-verify                skip the verification of any upstream TLS (default: true)
   --cors-origins value                      origins to add to the CORE origins control (Access-Control-Allow-Origin)
   --cors-methods value                      methods permitted in the access control (Access-Control-Allow-Methods)
   --cors-headers value                      set of headers to add to the CORS access control (Access-Control-Allow-Headers)
   --cors-exposed-headers value              expose cors headers access control (Access-Control-Expose-Headers)
   --cors-credentials                        credentials access control header (Access-Control-Allow-Credentials) (default: false)
   --cors-max-age value                      max age applied to cors headers (Access-Control-Max-Age) (default: 0s)
   --hostnames value                         list of hostnames the service will respond to
   --store-url value                         url for the storage subsystem, e.g redis://127.0.0.1:6379, file:///etc/tokens.file
   --encryption-key value                    encryption key used to encryption the session state [$PROXY_ENCRYPTION_KEY]
   --invalid-auth-redirects-with-303         use HTTP 303 redirects instead of 307 for invalid auth tokens (default: false)
   --no-redirects                            do not have back redirects when no authentication is present, 401 them (default: false)
   --skip-token-verification                 TESTING ONLY; bypass token verification, only expiration and roles enforced (default: false)
   --upstream-keepalives                     enables or disables the keepalive connections for upstream endpoint (default: true)
   --upstream-timeout value                  maximum amount of time a dial will wait for a connect to complete (default: 10s)
   --upstream-keepalive-timeout value        specifies the keep-alive period for an active network connection (default: 10s)
   --upstream-tls-handshake-timeout value    the timeout placed on the tls handshake for upstream (default: 10s)
   --upstream-response-header-timeout value  the timeout placed on the response header for upstream (default: 10s)
   --upstream-expect-continue-timeout value  the timeout placed on the expect continue for upstream (default: 10s)
   --verbose                                 switch on debug / verbose logging (default: false)
   --enabled-proxy-protocol                  enable proxy protocol (default: false)
   --max-idle-connections value              max idle upstream / keycloak connections to keep alive, ready for reuse (default: 0)
   --max-idle-connections-per-host value     limits the number of idle connections maintained per host (default: 0)
   --server-read-timeout value               the server read timeout on the http server (default: 10s)
   --server-write-timeout value              the server write timeout on the http server (default: 10s)
   --server-idle-timeout value               the server idle timeout on the http server (default: 2m0s)
   --use-letsencrypt                         use letsencrypt for certificates (default: false)
   --letsencrypt-cache-dir value             path where cached letsencrypt certificates are stored (default: "./cache/")
   --sign-in-page value                      path to custom template displayed for signin
   --forbidden-page value                    path to custom template used for access forbidden
   --tags value                              keypairs passed to the templates at render,e.g title=Page
   --forwarding-username value               username to use when logging into the openid provider [$PROXY_FORWARDING_USERNAME]
   --forwarding-password value               password to use when logging into the openid provider [$PROXY_FORWARDING_PASSWORD]
   --forwarding-domains value                list of domains which should be signed; everything else is relayed unsigned
   --disable-all-logging                     disables all logging to stdout and stderr (default: false)
   --help, -h                                show help
   --version, -v                             print the version

あとは、ドキュメントの記載を参考に。

Configuration options

Example usage and configuration

起動。

$ ./keycloak-gatekeeper --config gatekeeper.yml

ここまでで、Keycloak Gatekeeperの準備は完了です。

確認

では、確認してみましょう。

認証が不要な「http://172.17.0.2:3000/public/index」にアクセスしてみます。

f:id:Kazuhira:20190223193220p:plain

アクセスできました。

では、認証が必要な「http://172.17.0.2:3000/secure/all」にアクセスしてみます。Keycloakのログイン画面が表示されるので、
今回は「user001」でログイン。

f:id:Kazuhira:20190223193326p:plain

ログインに成功すると、リダイレクトされ「http://172.17.0.2:3000/secure/all」にアクセスできます。

f:id:Kazuhira:20190223193424p:plain

テキストでは、こんな感じです。

{
  "path": "/secure/all",
  "message": "Hello Secure Area!!",
  "headers": {
    "host": "192.168.0.2:5000",
    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
    "accept-language": "ja,en-US;q=0.9,en;q=0.8",
    "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiaFRIWWZnRHUyRHhITjJJWXowVWpnQ1RjOG1lSGNyclV3NE5aT1Y5QW1VIn0.eyJqdGkiOiJjNmJkMWIyMy0xZTRkLTQyZGYtYTgwNS0zZWE1MTFkYmFkM2QiLCJleHAiOjE1NTA5MTgzMjgsIm5iZiI6MCwiaWF0IjoxNTUwOTE4MDI4LCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMzo4MDgwL2F1dGgvcmVhbG1zL2RlbW8tYXBpIiwiYXVkIjpbInNhbXBsZS1nYXRla2VlcGVyLWFwaSIsImFjY291bnQiXSwic3ViIjoiZDc3MzgyYjYtMTNiNC00NzMzLTlhZDEtM2YxNGNjMTkxZTNkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2FtcGxlLWdhdGVrZWVwZXItYXBpIiwiYXV0aF90aW1lIjoxNTUwOTE4MDI4LCJzZXNzaW9uX3N0YXRlIjoiZTM4OTAyZDktY2M1ZC00YTQxLWIyMDctZWQ3MDBhYWY0NjM3IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vMTcyLjE3LjAuMjozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIwMDEifQ.UjMLf21bUtfshItkqJ4JFkCLlgQNlN-nvfJgSaKGwHxl2YgUZgXGyvIcB37uDg9OSMgqrli7GFDnblDitHmWClV1KNRG3ohjuB86d3vNL-smBAOKAY6BhEiGN967LXSlvA1iMVZkyWmkoJqv_7KgmOuGOlduot4uF11neALCVIvFdwwsSbxJfWYQPdTFg4WB9rORa_1q11qrrWLwln3smazj-zBQR_29xWrFPHjlEg0Twpz28nT9DyR-qFieJvEQhHf0mLnEGCuscycaw2rQOjAwHFmr1BID_uk61W0LPLYzlt2XoOPaYARrPcAjwXKdQGquEDcicXrpUd4gxKepxA",
    "cache-control": "max-age=0",
    "cookie": "request_uri=L3NlY3VyZS9hbGw=; OAuth_Token_Request_State=1b5fab5f-4f23-4b9d-800c-c8a0ca2328f4; kc-access=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiaFRIWWZnRHUyRHhITjJJWXowVWpnQ1RjOG1lSGNyclV3NE5aT1Y5QW1VIn0.eyJqdGkiOiJjNmJkMWIyMy0xZTRkLTQyZGYtYTgwNS0zZWE1MTFkYmFkM2QiLCJleHAiOjE1NTA5MTgzMjgsIm5iZiI6MCwiaWF0IjoxNTUwOTE4MDI4LCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMzo4MDgwL2F1dGgvcmVhbG1zL2RlbW8tYXBpIiwiYXVkIjpbInNhbXBsZS1nYXRla2VlcGVyLWFwaSIsImFjY291bnQiXSwic3ViIjoiZDc3MzgyYjYtMTNiNC00NzMzLTlhZDEtM2YxNGNjMTkxZTNkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2FtcGxlLWdhdGVrZWVwZXItYXBpIiwiYXV0aF90aW1lIjoxNTUwOTE4MDI4LCJzZXNzaW9uX3N0YXRlIjoiZTM4OTAyZDktY2M1ZC00YTQxLWIyMDctZWQ3MDBhYWY0NjM3IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vMTcyLjE3LjAuMjozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIwMDEifQ.UjMLf21bUtfshItkqJ4JFkCLlgQNlN-nvfJgSaKGwHxl2YgUZgXGyvIcB37uDg9OSMgqrli7GFDnblDitHmWClV1KNRG3ohjuB86d3vNL-smBAOKAY6BhEiGN967LXSlvA1iMVZkyWmkoJqv_7KgmOuGOlduot4uF11neALCVIvFdwwsSbxJfWYQPdTFg4WB9rORa_1q11qrrWLwln3smazj-zBQR_29xWrFPHjlEg0Twpz28nT9DyR-qFieJvEQhHf0mLnEGCuscycaw2rQOjAwHFmr1BID_uk61W0LPLYzlt2XoOPaYARrPcAjwXKdQGquEDcicXrpUd4gxKepxA; kc-state=Caz2Dq85N44pnb6kbI5/w2K2MDlpZ0lh6QEHNThAnfzh3iEFr3K0QVGKtSCUf8auEoQMNQRt3/LTHUzhLv+juCpXX7NA3f0A7QpnxfLiTOl4AvQRGMmCrgxgC3spaA0RckOhJxTnSYC3iAjBdJwSWkKnrs7j3bs73uI7P8ubQJ3YP1MKFYMHsQHfFgcXy9S0UBu7ruFUjxl9AM1LGE+1dHmMXO8lXSZlG4a1AjC7cSi44stx3Ssn5qdB5lS+wlzGy807d4p/NLZmYKf1K9xvfOiRgLoI9HKYDg0V4cBa4fJDB4BhqiRCb7cBOmgyTmyIgxor+isCb4pmZyDsZln8pg0qTbh3voPj1OqGr1iAlNyZk3uZGbBQZQC+t2xLaypRIjSHSwxn+Jump0zFbJv1JLbHg57IK4WJhM6EDcIBSFFTeu1vxLLVFd5EWYh07Cq4288cXWcdD+RVZXmnLwIdgJ55Q5J+Z12rnLxPbc5jD6WIx1GL5ae4T4w+TG7BG029PYIRMzWoNs62qHy6Ai8v9jGa1H9iZy3JsAicDQ9oqYVYYWyN/WCN+gawAi460e7CuWd2PZuxzV/tSmPlGNi+wEmkIRQqzObmr9mLHWUFoI7UDHhy3KeQkgmphKgzRPaO0JVU52Psc9cFVbV4/GFFLpA+H9Fj47BRMa6ed5TqGLXYzKHmlMBtH16pJdLKIxjNqP09CaYH4/Ej7bPSbOCMeigapiZrhz1trkfbjin2oLBysCiRoqILBtMV11bnK315dzmdD+ycyyaWZMknCDAFxkoaoLPuXk/3sv/lRxhwsgp2rOk5PEJyV2lmJSLms+EDRzsnnLaHNrIMJOcfja2VmGUTsOIzLh9Fvr8odk53/elUYgBCl9OZRPsCMapDeL7WkUhtXXuz2jYKuDeA79+YEUvy4WsQLtzzWd3FKNaG1CwH1cC7vGcyO9zWt/+WsvtHdBvR+JMOwkq23sbj+tH5hhmKH9Kkeq5XohYAc352+JbMjYuxJ8yfhBxeRIlONxERI0Rx01MKGRxItgRi3uwOegJyi7PkVPHE0TqwcxVD88LU9ggS4rVZRGWT/gWl+YpvZLtaspIToLm9qzn8P90rJw3/j6JMFP02DpqnNth1wmct+xcTm5p64ZGi47uLS1lGUKXGotk7YfgAEzPYGmt3ZsLnoPE2ZCYq8eNB9UtbZRbZKbUM63RgSD2hnBAmZg",
    "referer": "http://172.17.0.3:8080/auth/realms/demo-api/protocol/openid-connect/auth?client_id=sample-gatekeeper-api&redirect_uri=http%3A%2F%2F172.17.0.2%3A3000%2Foauth%2Fcallback&response_type=code&scope=openid+email+profile&state=1b5fab5f-4f23-4b9d-800c-c8a0ca2328f4",
    "upgrade-insecure-requests": "1",
    "x-auth-audience": "sample-gatekeeper-api,account",
    "x-auth-email": "",
    "x-auth-expiresin": "2019-02-23 10:38:48 +0000 UTC",
    "x-auth-groups": "",
    "x-auth-roles": "offline_access,uma_authorization,user,account:manage-account,account:manage-account-links,account:view-profile",
    "x-auth-subject": "d77382b6-13b4-4733-9ad1-3f14cc191e3d",
    "x-auth-token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiaFRIWWZnRHUyRHhITjJJWXowVWpnQ1RjOG1lSGNyclV3NE5aT1Y5QW1VIn0.eyJqdGkiOiJjNmJkMWIyMy0xZTRkLTQyZGYtYTgwNS0zZWE1MTFkYmFkM2QiLCJleHAiOjE1NTA5MTgzMjgsIm5iZiI6MCwiaWF0IjoxNTUwOTE4MDI4LCJpc3MiOiJodHRwOi8vMTcyLjE3LjAuMzo4MDgwL2F1dGgvcmVhbG1zL2RlbW8tYXBpIiwiYXVkIjpbInNhbXBsZS1nYXRla2VlcGVyLWFwaSIsImFjY291bnQiXSwic3ViIjoiZDc3MzgyYjYtMTNiNC00NzMzLTlhZDEtM2YxNGNjMTkxZTNkIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoic2FtcGxlLWdhdGVrZWVwZXItYXBpIiwiYXV0aF90aW1lIjoxNTUwOTE4MDI4LCJzZXNzaW9uX3N0YXRlIjoiZTM4OTAyZDktY2M1ZC00YTQxLWIyMDctZWQ3MDBhYWY0NjM3IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vMTcyLjE3LjAuMjozMDAwIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbGUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InVzZXIwMDEifQ.UjMLf21bUtfshItkqJ4JFkCLlgQNlN-nvfJgSaKGwHxl2YgUZgXGyvIcB37uDg9OSMgqrli7GFDnblDitHmWClV1KNRG3ohjuB86d3vNL-smBAOKAY6BhEiGN967LXSlvA1iMVZkyWmkoJqv_7KgmOuGOlduot4uF11neALCVIvFdwwsSbxJfWYQPdTFg4WB9rORa_1q11qrrWLwln3smazj-zBQR_29xWrFPHjlEg0Twpz28nT9DyR-qFieJvEQhHf0mLnEGCuscycaw2rQOjAwHFmr1BID_uk61W0LPLYzlt2XoOPaYARrPcAjwXKdQGquEDcicXrpUd4gxKepxA",
    "x-auth-userid": "user001",
    "x-auth-username": "user001",
    "x-forwarded-for": "172.17.0.1",
    "x-forwarded-host": "172.17.0.2:3000",
    "x-forwarded-proto": "",
    "accept-encoding": "gzip"
  }
}

続いて、「http://172.17.0.2:3000/secure/user」へアクセス。表示される画面の内容は、先ほどと変わらないので割愛。

では、「http://172.17.0.2:3000/secure/admin」にアクセス。

こちらは、表示する権限がありません。

f:id:Kazuhira:20190223193601p:plain

ここで1度Cookieを消し、Keycloakからもログアウトして、今度は「admin001」ユーザーでログインし直します。
※Keycloakからのログアウトには、Sessionsから強制ログアウトさせました

すると、今度は参照可能になります。

f:id:Kazuhira:20190223193754p:plain

今回はadminロールを持つユーザーに、userロールを持たせていないので、ロールでアクセス制限を行っているリソースへは、
どちらか片方しかアクセスできませんが…。

トラブルシュート

ログインしたのに保護されたリソースにうまくアクセスできない場合は、Protocol Mapperの設定が疑われます。

以下のようなログが、Keycloak Gatekeeperに出力されていないか確認しましょう。

1.5501313610580595e+09   error   unable to verify the id token   {"error": "oidc: JWT claims invalid: invalid claim value: 'aud' is required, and should be either string or string array"}

「aud」(Audience:トークンの受け手)を表すクレームが必要だよ、と言われています。

先述したProtocol Mapperは、ここに効いてきます。

There is a known issue with the Keycloak server 4.7.0.Final in which Gatekeeper is unable to find the client_id in the aud claim. This is due to the fact the client_id is not in the audience anymore. The workaround is to add the "Audience" protocol mapper to the client with the audience pointed to the client_id.

Known Issues

制限

Kecloak Gatekeeperの制限。Cookieに関するものが書かれていますね。

Limitations

まとめ

今回は、Keycloak Gatekeeperを試してみました。「aud」クレームで若干ハマり、Keycloak自体でハマったものの、なんとか
動かすことはできました。

動かしていて、まだまだOpenID Connectとかわかってないなぁということを強く思ったので、勉強しないと…。