CLOVER🍀

That was when it all began.

KeycloakのNode.js Adapterを使ってOpenID Connect

以前に、KeycloakのJava Servlet Filter Adapterを使って、OpenID Connectを試してみました。

KeycloakのJava Servlet Filter Adapterを使ってOpenID Connect - CLOVER

今度は、Node.jsのAdapterを使って試してみたいと思います。

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で作成した内容を模したものとします。

環境

各種バージョン。

$ node -v
v9.11.1

$ npm -v
5.6.0

準備

まずは、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

express-session

どうやら、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

User

  • User name … api-user

まあ、基本的には先のエントリと同じです。

Root URLのリッスンポートが3000になっているくらいですね。

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

では、ドキュメントを見ながらサンプルを書いてみます。

Node.js Adapter / Usage

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/」戻ってきました。

まとめ

KeycloakのNode.js Adapterを使って、Node.jsでOpenID Connectを試してみました。

Express.jsとexpress-session、そしてOpenID Connect自体にまだ明るくないのでだいぶハマりましたが、ちょっと理解が進んだのではないかと思います…。