以前に、KeycloakのJava Servlet Filter Adapterを使って、OpenID Connectを試してみました。
KeycloakのJava Servlet Filter Adapterを使ってOpenID Connect - CLOVER
今度は、Node.jsのAdapterを使って試してみたいと思います。
Keycloak Node.js Adapter
Node.jsで使うことができるKeycloakのAdapterで、Express.jsなどのフレームワークと統合ができるようです。
ドキュメントにおいてもExpress.jsを使った例が書かれていますので、今回もこちらに習ってみましょう。
Express - Node.js web application framework
基本的に、作成プログラムは先のJava Servlet Filter Adapterで作成した内容を模したものとします。
準備
まずは、KeycloakのNode.js Adapterのインストール。今回は、3.4.3を選択します。
$ npm i --save keycloak-connect@3.4.3
Node.js Adapter / Installation
続いて、Express.jsとexpress-sessionをインストール。
$ npm i --save express express-session
"dependencies": { "express": "^4.16.3", "express-session": "^1.15.6", "keycloak-connect": "^3.4.3" }
Express - Node.js web application framework
どうやら、KeycloakのNode.js Adapterを使う際には、このexpress-sessionが必要なようです。
Keycloakの準備
基本的には、こちらと同じです。
KeycloakのJava Servlet Filter Adapterを使ってOpenID Connect - CLOVER
起動。
$ bin/standalone.sh -Djboss.bind.address=0.0.0.0 -Djboss.bind.address.management=0.0.0.0
ユーザー作成と再起動。
$ bin/add-user-keycloak.sh -u keycloak-admin -p password $ bin/add-user.sh -u admin -p password $ bin/jboss-cli.sh -c -u=admin -p=password --command=reload
Realmなどの情報は、以下のようにしました。
Realm
- name … demo-api
Client
- Client ID … sample-rest-api
- Client Protocol … openid-connect
- Root URL … http://localhost:3000/
- Access Type … confidential
User
- User name … api-user
まあ、基本的には先のエントリと同じです。
Root URLのリッスンポートが3000になっているくらいですね。
サンプルアプリケーション
では、ドキュメントを見ながらサンプルを書いてみます。
Node.js Adapter / Installing Middleware
Node.js Adapter / Protecting Resources
Node.js Adapter / Additional URLs
で、書いたコードがこちら。
src/server.js
const express = require("express"); const session = require("express-session"); const Keycloak = require("keycloak-connect"); const sessionStore = new session.MemoryStore(); const keycloak = new Keycloak({ store: sessionStore }); const app = express(); app.use(session({ secret: "secret-sign", resave: false, saveUninitialized: false })); app.use(keycloak.middleware()); // change logout url // app.use( keycloak.middleware( { logout: "/logoff" } )); // public url app.get("/", (req, res) => res.send("Welcome!!")); app.get("/public/hello", (req, res) => res.send("Hello")); app.get("/public/keycloak-id-token", (req, res) => { if (req.kauth.grant_id) { res.send(req.kauth.grant.id_token.content); } else { res.send("no content"); } }); app.get("/public/keycloak-token", (req, res) => { if (req.session['keycloak-token']) { res.send(req.session['keycloak-token']); } else { res.send("no keycloak-token"); } }); // protecte url app.get("/auth/success", keycloak.protect(), (req, res) => { res.send(`Success Authentication ${req.kauth.grant.id_token.content.preferred_username}!!`); }); app.get("/auth/keycloak-token", keycloak.protect(), (req, res) => { res.send(req.session['keycloak-token']); }); app.get("/auth/keycloak-id-token", keycloak.protect(), (req, res) => { res.send(req.kauth.grant.id_token.content); }); app.listen(3000, () => console.log(`[${new Date()}] server, startup`));
最初の基本的な設定は、こちらを見ながら。
Node.js Adapter / Usage
Node.js Adapter / Installing Middleware
const express = require("express"); const session = require("express-session"); const Keycloak = require("keycloak-connect"); const sessionStore = new session.MemoryStore(); const keycloak = new Keycloak({ store: sessionStore }); const app = express(); app.use(session({ secret: "secret-sign", resave: false, saveUninitialized: false })); app.use(keycloak.middleware()); // change logout url // app.use( keycloak.middleware( { logout: "/logoff" } ));
Keycloakのインスタンスを作成する際に、SessionStoreを渡すのですが、この時にKeycloakの設定ファイルの内容も渡す必要があります。
const keycloak = new Keycloak({ store: sessionStore });
デフォルトでは、カレントディレクトリの「keycloak.json」ファイルを読もうとします。
keycloak.json
{ "realm": "demo-api", "resource": "sample-rest-api", "auth-server-url": "http://172.17.0.2:8080/auth", "credentials": { "secret": "dcd7d437-aef6-4137-b043-0d274496c817" } }
「credentials / secret」は、ClientのConfidentialsタブのSecretの値を使用します。
keycloak.jsonを用意せず、オブジェクトで直接指定する場合に付いてはドキュメントを参照してください。
ちょっと順番前後しますが、ログアウトのパスはデフォルトでは「/logout」だそうで、これをカスタマイズするにはKeycloak#middlewareを呼び出す際に、
コメントの様にlogoutパラメーターを設定すればよいみたいです。
// change logout url app.use( keycloak.middleware( { logout: "/logoff" } ));
続いて、Keycloakで保護するパスと保護しないパスを設定します。
最初にKeycloakで保護しない(誰でもアクセスできる)パスから。
// public url app.get("/", (req, res) => res.send("Welcome!!")); app.get("/public/hello", (req, res) => res.send("Hello")); app.get("/public/keycloak-id-token", (req, res) => { if (req.kauth.grant_id) { res.send(req.kauth.grant.id_token.content); } else { res.send("no content"); } }); app.get("/public/keycloak-token", (req, res) => { if (req.session['keycloak-token']) { res.send(req.session['keycloak-token']); } else { res.send("no keycloak-token"); } });
「/」ともうひとつは、まあいいでしょう。
app.get("/", (req, res) => res.send("Welcome!!")); app.get("/public/hello", (req, res) => res.send("Hello"));
こちらは、リクエスト内に保持されているKeycloakから引き抜いたユーザーの情報になります。取得できれば、の話ですが。
app.get("/public/keycloak-id-token", (req, res) => { if (req.kauth.grant_id) { res.send(req.kauth.grant.id_token.content); } else { res.send("no content"); } });
Keycloakで保護しているパスではありませんが、この説明はまた後で。
続いて、セッションから抜き出す場合。
app.get("/public/keycloak-token", (req, res) => { if (req.session['keycloak-token']) { res.send(req.session['keycloak-token']); } else { res.send("no keycloak-token"); } });
こちらも、また後で。
Keycloakで保護するパス。
// protecte url app.get("/auth/success", keycloak.protect(), (req, res) => { res.send(`Success Authentication ${req.kauth.grant.id_token.content.preferred_username}!!`); }); app.get("/auth/keycloak-token", keycloak.protect(), (req, res) => { res.send(req.session['keycloak-token']); }); app.get("/auth/keycloak-id-token", keycloak.protect(), (req, res) => { res.send(req.kauth.grant.id_token.content); });
Keycloakでパスを保護する場合、Keycloak#protectを使います。
Node.js Adapter / Protecting Resources
app.get("/auth/success", keycloak.protect(), (req, res) => {
で、Keycloakから取得した情報にどうやってアクセスするか、ですが、特にドキュメントに書かれていなかったのでGitHubにあるサンプルを参考にしました。
https://github.com/keycloak/keycloak-nodejs-connect/blob/v3.4.3/example/index.js#L79
セッションから取得するらしいです。
res.send(req.session['keycloak-token']);
ただ、これだけだとユーザーの情報が見れなさそうな感じだったので、ソースコードを見ているとreq.kauth.grantから取得すればよさそうな感じ。
https://github.com/keycloak/keycloak-nodejs-connect/blob/v3.4.3/middleware/protect.js#L52
res.send(req.kauth.grant.id_token.content);
ホントかな?
確認してみる
では、ちょっと確認してみましょう。
起動。
$ node src/server.js
ログイン前は、「http://localhost:3000/」と「http://localhost:3000/public/hello」は割愛。
Keycloakの情報にアクセスするURLで、確認してみましょう。
http://localhost:3000/public/keycloak-id-token
http://localhost:3000/public/keycloak-token
当然、中身はありません。
では、認証が必要なページにアクセスしてみます。
http://localhost:3000/auth/success
1度Keycloakのログイン画面をはさむので、作成しておいた「api-user」でログインします。
コードは、こちら。
app.get("/auth/success", keycloak.protect(), (req, res) => { res.send(`Success Authentication ${req.kauth.grant.id_token.content.preferred_username}!!`); });
では、Keycloakからの情報にアクセスしてみましょう。
http://localhost:3000/auth/keycloak-id-token
app.get("/auth/keycloak-id-token", keycloak.protect(), (req, res) => { res.send(req.kauth.grant.id_token.content); });
http://localhost:3000/auth/keycloak-token
app.get("/auth/keycloak-token", keycloak.protect(), (req, res) => { res.send(req.session['keycloak-token']); });
ユーザー名などの情報が取得できているのは、こちらのコードになります。
app.get("/auth/keycloak-id-token", keycloak.protect(), (req, res) => { res.send(req.kauth.grant.id_token.content); });
この状態で、Keycloakで保護されていない「http://localhost:3000/public/keycloak-token」にアクセスすると、こういう感じになります。
app.get("/auth/keycloak-token", keycloak.protect(), (req, res) => { res.send(req.session['keycloak-token']); });
ただ、こちらはrequestから取得するので、変化がありません。
http://localhost:3000/public/keycloak-id-token
さて、Keycloakにログイン後、Keycloakで保護されていないパスにアクセスした際にユーザーの情報を取得するにはどうしたらいいのでしょう?
ここで、OpenID Connect 1.0の仕様を見ると、「id_token」をbase64urlでデコードすればよいと。
[http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken:title=ID Token」
さらに言うと、「id_token」は「JWS Compact Serialization」という形式の「.」で区切られた形式で、それぞれ順番に「ヘッダー」、「ペイロード」、「署名」で
構成されるのだとか。
JWS Compact Serialization
この「ペイロード」の部分をbase64urlでデコードすればよさそうです。
では、ちょっと変えてみましょう。「base64-url」インストール。
base64-url
$ npm i --save base64-url
依存関係は、このように。
"dependencies": { "base64-url": "^2.2.0", "express": "^4.16.3", "express-session": "^1.15.6", "keycloak-connect": "^3.4.3" }
base64urlを使うように、コードを追加。
const base64url = require("base64-url");
セッションから抜き出した「id_token」のうち、ペイロードの部分をbase64urlでデコードしてみます。
app.get("/public/keycloak-token", (req, res) => { if (req.session['keycloak-token']) { const idToken = JSON.parse(req.session["keycloak-token"])["id_token"]; const payload = idToken.split(".")[1]; res.send(base64url.decode(payload)); } else { res.send("no keycloak-token"); } });
で、アプリケーションを再起動して、Keycloakにログインして再確認。
http://localhost:3000/public/keycloak-token
今度は、ユーザーの情報が取得できました。
*「preferred_username」が見えています
というか、id_tokenをデコードするのが正解なんでしょうか?
最後、ログアウト。
http://localhost:3000/logout
ログアウト後、リダイレクトして「http://localhost:3000/」戻ってきました。