Back

ElectronアプリへのAuthentication追加方法

ElectronアプリへのAuthentication追加方法

ElectronアプリへのAuthentication追加方法を検索したことがある方なら、ログインフォームを表示するためにBrowserWindowを開き、Electron内でOAuthコールバックをインターセプトし、トークンをプレーンテキストで保存するチュートリアルを見かけたことがあるでしょう。このアプローチは時代遅れであり、セキュリティ上の問題があるだけでなく、現在のネイティブアプリ向けOAuth 2.0の仕様とも相容れません。

本記事では、正しいアーキテクチャを解説します。具体的には、PKCE(Proof Key for Code Exchange)を用いたAuthorization Code Flow、システムブラウザを通じたログイン、そしてディープリンクまたはループバックリダイレクトによるトークンのアプリへの返却です。

重要なポイント

  • 埋め込みBrowserWindowによるログインフローはRFC 8252に違反し、ユーザーの認証情報が傍受されるリスクがあります。代わりにシステムブラウザを使用してください。
  • PKCE(Proof Key for Code Exchange)はクライアントシークレットの代替手段です。Electronアプリではクライアントシークレットを安全に保管できません。
  • カスタムURIスキームによるディープリンク、またはループバックリダイレクトを使用して、認可コードをメインプロセスに返却します。
  • リフレッシュトークンはsafeStorageを使用して保存します。これにより、OSレベルのキー管理(Keychain、DPAPI、libsecret)が活用されます。
  • contextIsolation: trueでレンダラープロセスを分離し、contextBridgeを通じて必要最小限のIPCメソッドのみを公開します。

ElectronのAuthenticationがWebのAuthenticationと異なる理由

Electronアプリはユーザーが管理するマシン上で動作します。クライアントシークレットを隠すためのバックエンドが存在せず、アプリをオペレーティングシステムから分離するブラウザサンドボックスもありません。これにより、脅威モデルが大きく変わります。

ネイティブアプリおよびデスクトップアプリ向けに策定されたOAuth 2.0標準仕様であるRFC 8252は明確に述べています。OAuthログインフローに埋め込みWebビューを使用してはならないと。埋め込みBrowserWindowは認証情報を密かにインターセプトできるため、ユーザーは正規のIDプロバイダーと通信しているかどうかを確認する手段がありません。代わりにシステムブラウザを使用してください。

正しいアーキテクチャ:ディープリンクを用いたOAuth 2.0 PKCE

推奨されるフローは以下のとおりです。

  1. ユーザーがElectronアプリで「ログイン」をクリックします。
  2. アプリが認可URLを使用してシステムブラウザを開きます。
  3. ユーザーがIDプロバイダー(Auth0、Clerk、Firebase Authentication、AWS Cognito、またはOpenID Connect互換プロバイダー)で認証を行います。
  4. プロバイダーがmyapp://auth/callbackのようなカスタムURIスキーム、またはhttp://127.0.0.1:PORT/callbackのようなループバックアドレスにリダイレクトします。
  5. Electronのメインプロセスがそのリダイレクトをインターセプトし、認可コードをトークンと交換します。
  6. トークンはsafeStorageを使用して安全に保存されます。

Electronアプリはクライアントシークレットを安全に保管できないため、**PKCE(Proof Key for Code Exchange)**がシークレットを完全に置き換えます。フロー開始前に、アプリはランダムなcode_verifierを生成し、それをハッシュ化してcode_challengeを作成し、認可リクエストとともにチャレンジを送信します。コードをトークンと交換する際には、元のcode_verifierを送信します。認可サーバーはこれらが一致することを検証します。シークレットがアプリの外部に漏れることはありません。

OAuthコールバック用ディープリンクの設定

メインプロセスでカスタムプロトコルハンドラーを登録します。

// main.js
const { app } = require('electron');
const path = require('path');

if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('myapp', process.execPath, [
      path.resolve(process.argv[1]),
    ]);
  }
} else {
  app.setAsDefaultProtocolClient('myapp');
}

// macOS: ディープリンクコールバックを処理する
app.on('open-url', (event, callbackUrl) => {
  event.preventDefault();
  handleAuthCallback(callbackUrl); // コードを解析し、トークンと交換する
});

// WindowsおよびLinux: ディープリンクはsecond-instanceを通じて届く
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (event, argv) => {
    const callbackUrl = argv.find((arg) => arg.startsWith('myapp://'));
    if (callbackUrl) handleAuthCallback(callbackUrl);
  });
}

WindowsおよびLinuxでは、second-instanceイベントがディープリンクをコマンドライン引数として渡します。macOSでは、open-urlが正しいイベントです。パッケージング時は、electron-builderでプロトコルを設定します。

{
  "protocols": {
    "name": "myapp",
    "schemes": ["myapp"]
  }
}

注意: macOSおよびLinuxでは、プロトコルハンドラーをオペレーティングシステムに登録する必要があるため、ディープリンクはパッケージ化されたアプリでのみ安定して動作します。ディープリンクのテストは、electron .ではなく、必ずパッケージ化されたビルドに対して行ってください。

Electron safeStorageを使用したトークンの安全な保存

トークンをlocalStorage、プレーンファイル、または暗号化なしのelectron-storeに保存しないでください。OSレベルのキー管理(macOSではKeychain、WindowsではDPAPI、LinuxではLibsecret)を使用してデータを暗号化するsafeStorageを使用してください。

const { app, safeStorage } = require('electron');
const fs = require('fs');
const path = require('path');

const TOKEN_PATH = path.join(app.getPath('userData'), 'refresh.enc');

async function saveRefreshToken(token) {
  if (!safeStorage.isEncryptionAvailable()) {
    throw new Error('Encryption is not available on this system');
  }

  const encrypted = await safeStorage.encryptStringAsync(token);
  fs.writeFileSync(TOKEN_PATH, encrypted);
}

async function loadRefreshToken() {
  if (!fs.existsSync(TOKEN_PATH)) return null;

  const encrypted = fs.readFileSync(TOKEN_PATH);
  return await safeStorage.decryptStringAsync(encrypted);
}

暗号化の前に必ずsafeStorage.isEncryptionAvailable()を確認してください。特に、OSキーリングが設定されていない可能性があるLinuxでは重要です。一部のLinuxシステムでは、セキュアなキーリングが利用できない場合、Electronがbasic_textバックエンドにフォールバックすることがあります。safeStorage.getSelectedStorageBackend()でアクティブなバックエンドを確認し、必要に応じてユーザーに適切な警告を表示してください。

レンダラープロセスの分離

レンダラープロセスはトークンに直接アクセスすべきではありません。contextIsolationnodeIntegration: falseを使用し、レンダラーが必要とするものだけを限定的なcontextBridgeを通じて公開します。

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('auth', {
  login: () => ipcRenderer.invoke('auth:login'),
  logout: () => ipcRenderer.send('auth:logout'),
  getProfile: () => ipcRenderer.invoke('auth:get-profile'),
});

メインプロセスがすべてのトークン操作を処理します。レンダラーは名前付きIPCメソッドを呼び出すだけです。これにより、レンダラーが悪意あるコンテンツによって侵害された場合の影響範囲を最小限に抑えられます。

IDプロバイダーの選択

上記のアーキテクチャは、OpenID Connect互換のプロバイダーであればどれでも機能します。Electronアプリでよく使用される選択肢には、Auth0ClerkFirebase AuthenticationAWS Cognitoなどがあります。選択したプロバイダーでアプリをネイティブまたはデスクトップアプリケーションタイプとして登録し、カスタムURIスキームを許可されたリダイレクトURIとして設定してください。

避けるべきパターン

多くのチュートリアルでは、いまだに以下のパターンが紹介されています。

// ❌ このパターンは使用しないでください
const authWindow = new BrowserWindow({ /* ... */ });
authWindow.loadURL(authorizationUrl);
authWindow.webContents.session.webRequest.onBeforeRequest(
  { urls: ['http://localhost/callback*'] },
  ({ url }) => { /* トークンをインターセプトする */ }
);

このパターンはログインフローをElectron内に埋め込むため、RFC 8252に違反し、ユーザーがIDプロバイダーを確認できなくなります。多くのチュートリアルで推奨されていても、このパターンは避けてください。

まとめ

Electronアプリに正しくAuthenticationを追加する方法はシンプルです。システムブラウザを開き、PKCEを用いたOAuth 2.0を使用し、ディープリンクまたはループバックリダイレクトでコールバックを処理し、safeStorageでトークンを保存します。そして、すべてのトークンロジックからレンダラープロセスを分離します。このアーキテクチャはRFC 8252が推奨するものであり、ネイティブアプリケーションを登録する際にAuth0やClerkなどの現代的なプロバイダーが期待するものでもあります。

よくある質問

はい、使用できます。ループバックリダイレクトは、127.0.0.1上のローカルHTTPサーバーとランダムポートを使用してOAuthコールバックを受信します。OSレベルのプロトコル登録が不要なため、開発中に便利です。ただし、ログインのたびにサーバーを起動・停止する必要があり、一部のプロバイダーではダッシュボードでループバックURIを明示的に設定する必要があります。

はい、必要です。プロバイダーがクライアントシークレットを発行する場合でも、ユーザーがElectronバイナリから抽出できるため、アプリ内に埋め込むべきではありません。PKCEはシークレットの有無にかかわらず、認可コードの交換を保護します。アプリをネイティブまたはパブリッククライアントとして登録し、PKCEが必須でシークレットが不要な設定にしてください。

リフレッシュトークンのみをsafeStorageで保存してください。アクセストークンの有効期限が切れた場合、メインプロセスがプロバイダーのトークンエンドポイントにリフレッシュトークンを送信し、新しいアクセストークンを受け取ります。アクセストークンはメインプロセスのメモリ内にのみ保持し、レンダラーには渡さないでください。リフレッシュトークンが失効または期限切れになった場合は、ユーザーにシステムブラウザを通じて再ログインを促してください。

カスタムプロトコルハンドラーはオペレーティングシステムに登録する必要があり、その登録は通常、アプリのインストールまたはパッケージング時に行われます。`electron .`を実行するとElectronバイナリが直接起動されるため、OSはアプリが`myapp://`スキームを処理することを認識しません。ディープリンクのテストは、electron-builderや同様のツールで生成したパッケージ化されたビルドに対して必ず行ってください。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay