TL;DR
- Salesforceに接続するElectronアプリのOAuth2認可フローにはcode challenge/verifier (a.k.a. PKCE) 使いましょう
- 認可画面の表示にはBrowserWindowじゃなくってアプリ外部の標準ブラウザを使いましょう
ElectronでSalesforceクライアントアプリを作る
JavaScriptでOSネイティブのクライアントアプリケーションが作れるElectronはいろいろツールを作るのに便利ですね。Salesforceのクライアントを作りたい人も結構いるのではないでしょうか。でもPHPやRubyで作るようなよくあるWebアプリとは違って、クライアントアプリケーションです。API接続のための認証はどうやったらいいのでしょうか。
残念ながら世の中にはあまり最適な例が落ちていなかった&不適切な例が多いようなので、現時点でベストプラクティスと思われる方法についてこちらで記載します。
(ちなみに本記事ではElectronが接続するサービスをSalesforceに特化して話していますが、Salesforceに限らず他のプラットフォームでも同じ考え方は適用できるでしょう。もちろん他サービスプラットフォームでは残念ながらベストプラクティスでは対応できないケースもありますのでご注意ください)
バッドプラクティス
ベストプラクティスの前に、まずはこれやってはダメだよね、というバッドプラクティスを記載します。結構な例がこのプラクティスで実装されていますので、ご注意下さい。
ユーザ名・パスワードを入力する画面をElectronで作る
ユーザ名・パスワード入力画面をElectronアプリ側で作成して用意し、SOAP APIのlogin API、もしくはOAuth2 Resource Owner Password Credentialでリクエストを送信し、access tokenをゲットするケースです(某公式データローダアプリはつい最近までそんな感じでしたよね)。これには以下の残念な点があります。
- フィッシング(アプリの利用者には入力したパスワードがどのように使われるかわからない)
- SalesforceのログインでSSO連携をしている場合(e.g.SalesforceにADでログイン)など、Salesforceのユーザ名/パスワードは使えない
- refresh tokenを取得できないため、継続的なセッションが必要な場合は入力されたユーザ名・パスワードを保存しておく必要があり、セキュアではない
OAuth2 の認可画面をElectronのBrowserWindow機能で表示する
OAuth2の認可フローを利用してaccess tokenを取得することを目標としますが、認可画面の表示にElectron組み込みのBrowserWindowを使って実施するケースです。以下の残念な点があります。
- フィッシング(画面にはSalesforceのログイン画面が表示されるが、それが正規のものかどうかアプリの利用者には判定できない)
- すでにブラウザ側でログイン済みの場合でも毎回ログインプロンプトが表示されてしまう
- 1passwordやLastPassなどのパスワードマネージャユーティリティとの相性の悪さ
OAuth2 のトークン取得のために Client Secretをアプリに埋め込んで配布する
OAuth2の認可フローのうち、Authorization Codeフローを用いる場合、tokenの取得の際に通常Client Id と Client Secretを両方指定してリクエストするようにしている例が多いです。Electronアプリでもこのようにしているケースが見られますが、以下の残念な点があります。
- シークレットの漏洩。Electronアプリに特有ではないですが、クライアントアプリケーションではどんなに頑張って秘匿化したとしてもコード内のClient Secretをアプリの利用者が取得することが原理的に可能です。
クライアントアプリ利用者がOAuth2の接続アプリ情報(Client ID/Secret)を自ら設定できるようにする
上記のシークレット漏れ対策のために、そもそもユーザ自身が接続アプリ情報を作成し、Client Id/Secret を指定できるように工夫してアプリを配布するケースです。以下の残念な点があります。
- アプリの利用者がとても面倒。接続アプリ情報を自分で作らせるのはエンドユーザ向けに配布する予定のアプリでは厳しいです。
OAuth2 Implicitフローでトークンを取得する
Webアプリ上のJavaScriptクライアントなどでよく用いられるOAuthの認可フローの1つにImplicitフローというのがあります。こちらはもともとJavaScriptコードがpublicであることからClient Secretについては必要とされません。Electronアプリにも同じ方法が適用できますが、やはり以下の残念な点があります。
- Implicit Grantではrefresh tokenを取得できないため、セッションが切れる度に毎回認可画面を立ち上げる必要がある
ちなみにSalesforceのOAuth2では、custom schemeをredirect URLに持つユーザエージェントフローではなぜかrefresh tokenを取得できるように拡張されています。
が、しかしこれはOAuth2的にはセキュリティ違反です1。おそらくいずれかのタイミングで利用できなくなる可能性が高いのでは、と個人的には考えています。
ベストプラクティス
今までバッドプラクティスを見てきましたが、では実際にはどのようにしてSalesforceに対して接続をするのがよいのでしょうか。指針は以下のとおりです。
- API接続にはOAuth2 Authorization Code フローを利用してトークンを取得する
- OAuth2認可画面の表示には標準ブラウザウィンドウを立ち上げて表示する
- 認可コードフローの実行時にCode ChallengeおよびCode Verifierを含めて要求を行う
- 認可コードフローの実行時にClient Secretはリクエストに付与しない(=つまり配布するアプリには同梱しない)
- 認可コード受取(リダイレクトURI)の際には一時的なローカルHTTPサーバを立ち上げて対処する
Client Secretを必要としない 「Public Client」
OAuth2 Authorization Codeフローを利用するのは、やはりImplicitフローではトークン置換攻撃を受けるリスクが高いのと、(仕様上は)refresh tokenを取得できないという点がネックとなるためです。しかしAuthorization CodeフローではClient Id/Client Secretを必要としている例が多いため、Client Secretを使わないようにしようとするとこちらを選ぶしかなさそうに思えますが、実はAuthorization CodeフローでもClient Secretは必須というわけではありません。
OAuth2においては、Clientアプリケーションを認証するための秘密情報を機密に保持しておくことができないクライアントのことを「Public Client」と呼んでおり、今回のElectronアプリも含め、多くのモバイルアプリなどがこれに当たります。
このようなクライアントに対して、認証にシークレットを必要としないように設定しておく仕組みがサービスプロバイダ側にある場合があります。この設定がされたClient IDを持つクライアントがトークン取得リクエストを行った場合、今まで同時に提示していたClient Secretは必要がなくなります。
SalesforceでこのようなPublic Client用のクライアント登録を行うには、接続アプリケーションの作成の際に 「Require Secret for Web Server Flow」 のチェックを外してあげることで可能になります。

PKCEによるPublic Clientのセキュリティ強化
ところで、いままでclient secretを同時に送っていたから保たれていたセキュリティが、省くことによって何か壊れてしまうということはないのか、疑問に思う場合もあるかもしれません。たとえば、あるクライアントアプリケーションへの認可で得られたauthorization codeを、同一のクライアントIDを持つ別の悪意あるアプリケーションが横取りし、勝手にログインしてしまうことが可能になるかもしれません。
このような場合に、RFC7636で仕様化されているPKCE(Proof Key for Code Exchange)という仕組みが有効です。こちらはOAuth2の拡張仕様の1つですが、現在では多数のサービスで有効になりつつあります。簡単に言うと、認可画面のリクエスト時にチャレンジコードを生成して送付し、トークン取得する際にはチャレンジコードに対応した検証コードと共に送らないとトークン取得に失敗させることで、意図せぬクライアントからの割り込みや横取りを防ぐ仕組みです。
チャレンジコード、検証コードの生成については、ランダム関数やハッシュ関数(SHA256)など多少暗号処理的な知識が必要にはなりますが、内容的にそれほど難しいものではありません。以下にNode.jsでの生成例が載っていますので、Electronアプリでも同様にして利用できます。
標準ブラウザウィンドウで認可画面を表示しコールバックを受け取る
Electronのアプリの中にはBrowserWindowという組み込みのWeb画面表示の仕組みを用いてOAuth2のフローを行っているケースがあります。なぜこの方法が選ばれているかというと、元のメインプロセスで起ち上げたBrowserWindowのURLを監視しておくことで、リダイレクトURLに遷移したときにそのイベントを受け取ってOAuth2の認可フローを継続することができるためです(具体的にはdid-get-redirect-request
イベントあるいはwill-navigate
イベントを監視しているケースが多く見られます)。リダイレクトURLについては実際にホストされているアドレスでなくてもよいので利便性が高そうですが、先に述べた理由により組み込みブラウザでの認可画面表示は避けたほうがよいかと思います。
一方、認可画面を標準ブラウザで表示するようにした場合、Electronアプリの管理下ではないプロセスになりますので、URLを監視するといったやり方は難しくなります。なので実際にリダイレクトURLに送られてきたリクエストをElectronアプリがハンドルできるようにする必要があるのですが、この方法には大きく2通りあります。
- OSに対してカスタムURLスキームを登録し、Electronアプリがハンドルできるようにする。クライアントのリダイレクトURLには登録したカスタムURLスキームのURLを登録しておく
- localhostに一時的にWebサーバを立ち上げる。クライアントのリダイレクトURLにはlocalhostのアドレスを登録しておく。
1.の場合ですが、app.setAsDefaultProtocolClient()
APIを利用して利用するカスタムURLスキームを登録します。ただしMacの場合は事前にInfo.plistに記述しておく必要があるようです。さらにURLの受取についても、Macではopen-url
イベントの監視をする必要があるのに対し、Windowsではprocess.arvgを見る必要があるなど、いろいろと環境による差異が大きいところもあるので、こちらは2.がどうしても難しい場合に限ってもよいかと思います。
2.については、ElectronがほぼすべてのNode.jsのアプリケーションを動かせることを活かして、expressの簡易サーバを立ち上げてしまうことで対処できます。認可の処理中のみに限定できるのでポート番号の競合をあまり考える必要は少なさそうですが、それでも競合する場合もあるので、あまり使われなさそうな番号にしておくのがよいかと思います。
サンプルコード
以上のベストプラクティス2を実装したサンプルコードを置いておきます。どうぞご参考まで。
補足: sfdx CLIについて
ちなみに今回Electronアプリに限定して話してきましたが、シークレットを保持できないPublicなクライアントアプリケーションと言う意味では、ターミナルなどで動かすCLIプログラムでも同じことが言えるはずです。例えば、sfdxにおける認証方法の1つにforce:auth:web:login
というのがあったかとおもいますが、おそらくこちらも内部では同じことをやっているはずです3。
-
"The authorization server MUST NOT issue a refresh token" https://tools.ietf.org/html/rfc6749#page-35 ↩
-
もちろん現時点での個人的認識でしかありません。クールかつ有効な方法は他にもまだあるかもしれません ↩
-
詳しく見てませんからあくまで推測です ↩