Electron×openid-clientでOpen ID Connect認証を行う
今回はopenid-clientというモジュールを使ってElectronアプリでOpen ID Connect認証を行います。
前回書いたexpress-openid-connectじゃダメなのか?というところですが、以下の理由から今回は利用できませんでした。
- express-openid-connectはあまりにもなんでもモジュール内に閉じてしまっているため、たとえば「リフレッシュトークンを別で保存しておいて、セッション情報が消滅した後にリフレッシュだけ行う」みたいなことができない
- Electron19(Node16)の環境では動作しない(express-openid-connectのIssueで聞いてみたものの、Node16またはElectron18-19の仕様のようです)。
とりあえずコード
とにもかくにもとりあえず動作例。
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認証ではないので深掘りしませんでしたが対策はあるらしい。
なお、GithubもOpen ID Connect認証(っぽいもの?)は公開していますが、必要なAPIがそろっておらず、openid-clientでは接続ができないようです。
そして、リフレッシュ処理
そして、リフレッシュ処理です。
リフレッシュトークンを使ったリフレッシュ処理を行う場合は、BaseClient
オブジェクトを作り、オブジェクトのrefresh()
メソッドを呼び出せばOKです。
引数には、Open ID Connect認証時のコールバックより取得できるTokenSetオブジェクトまたは、リフレッシュトークンの文字列を渡せばOKです。
tokenSet = await client.refresh(tokenSet);
ちょっと使いづらいのでクラス化してみた
今回のままだとコードがバラバラで対応しづらいので、Open ID Connect認証処理をクラス化してみました。
こちらだとアプリの起動時に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