🔗

Electron×openid-clientでOpen ID Connect認証を行う

2022/07/19に公開

今回はopenid-clientというモジュールを使ってElectronアプリでOpen ID Connect認証を行います。

https://www.npmjs.com/package/openid-client

前回書いたexpress-openid-connectじゃダメなのか?というところですが、以下の理由から今回は利用できませんでした。

  • express-openid-connectはあまりにもなんでもモジュール内に閉じてしまっているため、たとえば「リフレッシュトークンを別で保存しておいて、セッション情報が消滅した後にリフレッシュだけ行う」みたいなことができない
  • Electron19(Node16)の環境では動作しない(express-openid-connectのIssueで聞いてみたものの、Node16またはElectron18-19の仕様のようです)。

とりあえずコード

とにもかくにもとりあえず動作例。

https://github.com/TakamiChie/openid-client-sample

sample.envを.envにリネームし、BASE_URLなどの値を設定してください。設定の内容については以前の記事、express-openid-connectを使ってOIDC接続を行うexpress-openid-connectを使ってGoogleログインを行うを参照してください。
その後、アプリを起動し、Authenticateボタンをクリックすると、Open ID Connect認証が実行されます。

動作の仕組み

openid-clientでも、express-openid-connectと同様、Expressと同時に使用するのがベターなようです。
ただし、express-openid-connectとは違って、Expressはあくまで認証のためのデータをローカルに持ってくるために動くのみで、express-openid-connectとopenid-clientは基本的に別々のオブジェクトとして動作します。

Clientオブジェクトの作成

まずは、ISSUER URLからの情報取得。ここはIssuerクラスのdiscover()メソッドで取得します。

  const issuer = await Issuer.discover(`${process.env.ISSUER_BASE_URL}`);
  console.log(issuer.issuer);
  console.log(issuer.metadata);

そして、取得したissuerオブジェクトを使って、BaseClientオブジェクトを作ります。

  const client = new issuer.Client({
    client_id: process.env.CLIENT_ID,    
    client_secret: process.env.CLIENT_SECRET,
    redirect_uris: [`${process.env.BASE_URL}callback`],
    response_types: ["code"],
  });

とりあえず、通信前にできるのはここまで。

認証URLをブラウザで開く

次に、認証用のURLをブラウザで開きます。
認証用のURLは、BaseClientオブジェクトのauthorizationUrl()メソッドを使用して取得できます。
認証URLアクセス時の脆弱性攻撃の対策に用いられるstateやnonce等の値は、openid-clientモジュール内にあるGeneratorsクラスのメソッドにより取得することができます。

  const state = generators.state();
  const nonce = generators.nonce();
  const code_verifier = generators.codeVerifier();
  const code_challenge = generators.codeChallenge(code_verifier);
  const authorizationUrl = client.authorizationUrl({
    scope: "openid email profile",
    state: state,
    nonce: nonce,
    response_type: "code",
    code_challenge: code_challenge,
    code_challenge_method: "S256",
  });
  await startServer(exp, process.env.PORT);
  shell.openExternal(authorizationUrl);

なお、最後のstartServer();メソッドは以下のようなもの。

async function startServer(expressApp, port){
  server = expressApp.listen(port, async() => {
    console.log("Start Listen!");
    mainWindow.webContents.send("log", "Server Start.", "info");
    return server;
  });
}

サーバの起動が完了するまで処理を待たせています。

コールバックを受け取る

そして、Expressで http://localhost/callback を待ち受け、そこでパラメータを解析する処理を行います。

  exp.get('/callback', async(req, res) => {
    const params = client.callbackParams(req);
    const tokenSet = await client.callback(`${process.env.BASE_URL}callback`, params, { code_verifier, state, nonce,  });
    const userInfo = await client.userinfo(tokenSet.access_token);
    res.send("Certification is completed.Close the tab."); 
    /* ここでメインウィンドウにコールバックする */
  });

通信がうまく行っていた場合、ここでuserInfo変数にユーザーの情報を示すオブジェクトが、tokenSet変数にアクセストークンやリクエストトークンなどの情報が格納されます。
なお、Googleの認証を使った場合、リフレッシュトークンが返送されない場合があります。今回の対象はGoogle認証ではないので深掘りしませんでしたが対策はあるらしい。

https://zenn.dev/yamadashy/scraps/68e40a81be36b1

なお、GithubもOpen ID Connect認証(っぽいもの?)は公開していますが、必要なAPIがそろっておらず、openid-clientでは接続ができないようです。

そして、リフレッシュ処理

そして、リフレッシュ処理です。

リフレッシュトークンを使ったリフレッシュ処理を行う場合は、BaseClientオブジェクトを作り、オブジェクトのrefresh()メソッドを呼び出せばOKです。
引数には、Open ID Connect認証時のコールバックより取得できるTokenSetオブジェクトまたは、リフレッシュトークンの文字列を渡せばOKです。

  tokenSet = await client.refresh(tokenSet);

ちょっと使いづらいのでクラス化してみた

今回のままだとコードがバラバラで対応しづらいので、Open ID Connect認証処理をクラス化してみました。

https://github.com/TakamiChie/openid-client-sample/tree/feature-oidc-class

こちらだとアプリの起動時にOIDCクラスをインスタンス化し、必要なときにdoAuthenticate()メソッドを実行、ブラウザを起動すればOKです。

  oidc = new OIDC(process.env.CLIENT_ID, parseInt(process.env.PORT), process.env.ISSUER_BASE_URL);
  oidc.codeAuth(process.env.CLIENT_SECRET);
  ///
  await oidc.doAuthenticate(onAuthenticate);
  shell.openExternal(oidc.authorization_url);

ついでに、このクラスではsaveToFile()メソッドとloadFromFile()メソッドを用意、トークン情報を暗号化してファイルに保存する処理を追加しています。

暗号化にはNode標準のcryptoというモジュールを使用。キーにはOSの名前やホスト名を利用しているので、すぐにはわからないようになっているはず・・・。

Discussion