Keycloakで、Express + Passport によるOIDC認証をする

皆さん、こんにちは。技術開発グループのn-ozawanです。
今年最後の投稿になります。今年一年、振り返ってどうでしたか?私は運動の習慣化に取り組みました。最近、腰の調子が良いです。

本題です。
今回は認証サーバーのKeycloakに対してOIDC認証を行います。言語はTypeScriptで、利用するパッケージはExpressとPassportになります。

TypeScript でOIDC認証

ゴール

Express + Passportで、Keycloakに対してOIDC認証を行います。付与方式は「認可コードによる認証方式」になります。なお、本稿ではKeycloak側の設定などは扱いません。正しく設定されていることを前提としています。

ExpressとPassport

ExpressはNode.jsで動作する軽量なHTTPサーバーです。Express自体に認証を行う機能はありませんが、HTTP通信をするために利用します。

PassportはExpressを利用して認証認可を実現するパッケージ群です。Passportは認証認可全般を扱う多くのパッケージで構成されており、今回利用するのは、passport-openidconnectとなります。passport-openidconnectはその名の通り、OIDC認証を実現するためのパッケージになります。特に説明がない限りは、本稿での「Passport」は「passport-openidconnect」を指します。

OIDC用の初期設定を行う

まずPassportでOIDC認証を利用するにあたり、OIDC認証に必要な情報をPassportへ与える必要があります。以下のコードはクラスOpenIDConnectStrategyで作成したインスタンスを、passport.useに渡しています。

  passport.use(
    new OpenIDConnectStrategy(
      // 第一引数
      {
        issuer: "http://localhost:8080/realms/myrealm",
        authorizationURL: "http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
        tokenURL: "http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
        userInfoURL: "http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo",
        clientID: "client.ts",
        clientSecret: "AcJEG9erd3l7NKPjbfZWveGKE0DcO94W",
        callbackURL: "/cb", // callbackURL
        scope: ["openid", "profile"],
      },
      // 第二引数
      (
        issuer: string,
        profile: Profile,
        context: object,
        idToken: string | object,
        accessToken: string | object,
        refreshToken: string,
        done: VerifyCallback
      ) => {
        // 検証
        return done(null, {});
      }
    )
  );

  // verify()で返却した内容をシリアライズしてセッションに一時格納する
  passport.serializeUser(function (user, done) {
    done(null, user);
  });

OpenIDConnectStrategyの第一引数に渡しているパラメータの意味は以下の通りです。

issuerIDトークンの発行者です。発行されたトークンの検証に用いられます。
authorizationURL認可エンドポイントです。
tokenURL認可コードからIDトークンとアクセストークンを取得するためのエンドポイントです。
userInfoURL認証したユーザー情報を取得するエンドポイントです。
clientIDクライアントIDです。Keycloakでクライアントを作成する際に入力したIDです。
clientSecretクライアントと認証サーバーの両者で保有する秘密の文字列です。
callbackURL認証後にクライアントへのコールバック先URLです。
scopeスコープです。

issuerauthorizationURLtokenURLuserInfoURLなどのURLは、以下のエンドポイントから取得することが出来ます。もしKeycloakへのURLがhttp://localhost:8080で、レルム名がmyrealmの場合、http://localhost:8080/realms/myrealm/.well-known/openid-configurationになります。

/realms/{realm-name}/.well-known/openid-configuration

OpenIDConnectStrategyの第二引数で指定している関数は、IDトークンおよびアクセストークンの取得後に行われる検証になります。もし検証して問題があれば、以下のように処理を中断します。

return done(new Error("error message."));

問題なければ以下のように処理を完了します。第2引数に空のオブジェクトを指定していますが、ここにはセッションに一時保存したい内容を指定します。本稿の主題から少し外れるため説明は省きますが、本来であればここにユーザー情報やトークンなどを指定します。

// 第二引数で指定した内容が→
return done(null, {});

// ここの第一引数userで指定される。
passport.serializeUser(function (user, done) {
  done(null, user);
});

authenticateを呼び出す

次に、Expressがユーザーからのリクエストを受信した際に、Passportに連携するようにします。連携は2つあり、1つ目は認証開始用、2つ目は認証サーバーからのコールバックです。

  // authenticate (1回目)
  server.get("/login", passport.authenticate("openidconnect"));

  // authenticate (2回目)
  server.get(
    "/cb",
    passport.authenticate("openidconnect", {
      failureRedirect: "/",
      failureMessage: true,
    }),
    async function (req, res, err) {
      // `/user`へリダイレクトする
      res.redirect("/user");
    }
  );

authenticate (1回目)では、/loginリクエストを受信した際に、Passportへ連携するようにしています。

authenticate (2回目)では、認証サーバーからのコールバック/cbリクエストを受信した際に、再度、Possportへ連携するようにしています。failureRedirectfailureMessageは、検証に失敗した場合の挙動になります。第3引数では、すべての認証が完了した際の処理を定義します。今回は単に/userにリダイレクトしています。

おわりに

単に認証するだけなら今回の実装で良さそうです。少ないコードで複雑な認証シーケンスが出来るのは便利ですね。しかし実際には、ユーザー情報やトークンをセッションに保存したり、認証したユーザーのロールを取得したり、追加の実装が必要になります。

皆様、よいお年を。

Recommendおすすめブログ