Salesforce OAuth 2.0連携でやらかした、リフレッシュトークン利用とクライアントタイプの誤解

Webサーバフローにおけるリフレッシュトークンと公開クライアントの誤解

OAuth 2.0で実際にやらかした判断は、Webサーバフロー(Authorization Code Flow)で refresh_token を安易に要求してしまったことだ。そして、それをPublic Client(公開クライアント)で利用できると思い込んでいたこと。当時の私は、クライアントタイプ(Confidential Client と Public Client)の区別が十分にできておらず、それが後々の設計変更につながった。

当時の状況と私の判断

あるプロジェクトで、JavaScriptベースのSPA(Single Page Application)からSalesforceに直接連携する必要があった。ユーザーはSalesforceにログイン済みで、そのセッションを利用してSPAからもデータにアクセスするという要件だ。

当初の私は、とにかくユーザーに再認証の手間をかけさせたくない、という思いが強かった。そのため、SalesforceのConnected App(接続アプリケーション)を設定する際に、迷わず offline_access スコープも追加した。これで refresh_token を取得し、将来的に access_token が期限切れになっても、ユーザーに再ログインを要求せずに済むと考えていた。

もちろん、セキュリティ意識が全くなかったわけではない。クライアント秘密鍵(client_secret)は非常に重要で、絶対にクライアントサイドに露出させてはならないことは理解していた。だから、Webサーバフローを選んだ。認証コードはフロントエンドで取得し、それをバックエンドAPIに送り、バックエンドで client_secret を使ってトークン交換を行う、という一般的なWebサーバアプリケーションの構成を頭の中で描いていたのだ。

何が問題だったか?:公開クライアントの特性の軽視

問題は、そのバックエンドAPIが「Salesforce連携のためだけのシンプルなプロキシ」のような位置づけで、永続的なトークンストアを持つような複雑なものではなかったことだ。そして、フロントエンドのJavaScriptアプリケーションが、あたかも単独でSalesforceと認証を完結させたいかのような設計を試みてしまった点にある。

具体的には、SPAから直接Salesforceの認証エンドポイントにリダイレクトし、認証コードを受け取った後、そのままクライアントサイドからSalesforceのトークンエンドポイントにリクエストを送ってトークンを交換しようとしてしまった。当時の私は、これを「シンプルで分かりやすい」と感じていた。

しかし、これはPublic Clientの典型的な振る舞いだ。そして、Public Clientは client_secret を安全に保管できないという本質的な制約がある。SalesforceのConnected Appでは、client_secret を使用しないPublic Clientの接続アプリケーションも作成できるが、その場合は通常 refresh_token の発行を伴わない、あるいはPKCE(Proof Key for Code Exchange)が必須となる。

私の場合は、Connected AppをPublic Clientとして設定したにも関わらず、offline_access スコープを要求し、さらには client_secret がバックエンドに存在することを前提としたトークン交換フローを、頭の中でごちゃ混ぜにしていたのだ。

  • 当時の判断の誤り:
    • SPA(Public Client)から直接認証フローを開始しつつ、refresh_token の取得を望んだ。
    • refresh_token の利用には通常 client_secret を使ったトークン交換が必要であるという点を、Public Clientの特性と絡めて深く考えていなかった。
    • 結果として、SPAが直接トークン交換を試みると client_secret が使えない。しかしバックエンドに client_secret があることを前提にフローを設計していたため、矛盾が生じた。

後から「やらなければよかった」と思った設計

後から振り返れば、「Public ClientであるSPAが直接 refresh_token を使ってアクセスを永続化させようとする」という設計そのものが、最初から誤りだった。client_secret を安全に管理できないクライアントが、長期的なアクセス権限である refresh_token を安易に利用しようとすること自体が、OAuth 2.0のセキュリティモデルから逸脱していたと言える。

最終的には、以下のどちらかの選択を迫られた。

  1. SPAから認証コードを取得後、必ずバックエンドAPIを介してトークン交換を行い、バックエンドAPIが client_secretrefresh_token を安全に管理し、SPAへのアクセスは短命な access_token かセッションIDを渡す。この場合、バックエンドAPIはConfidential Clientとなる。
  2. SPAが直接Salesforceと認証を行うPublic Clientとして動作するが、その場合はPKCEを導入し、client_secret なしで refresh_token を利用できるようにするか、そもそも refresh_token は使わず、 access_token の有効期限が切れたらユーザーに再認証を促す(Implicit Grant Flow のような挙動)。

当時の私は、バックエンドAPIを極力薄くしたかったため、1の選択肢を避けようとしていた。しかし、2の選択肢でPKCEの導入を検討した際に、「最初からPKCEを使えば、こんなことで悩まなかったのに」と激しく後悔したのを覚えている。

Connected Appの設定で、Webサーバフローで「PKCE を強制」というオプションがある。これを有効にしていれば、そもそも私の最初の判断ミスは未然に防げた可能性もある。

今なら別の選択をする

もし今、同じような要件に直面したら、迷わず以下の選択をするだろう。

  • Public Client (SPA) の場合:
    • Authorization Code Flow + PKCE を第一に検討する。client_secret は使用しない。
    • offline_access スコープ(refresh_token)は、本当にユーザーが再認証なしに長期間アクセスを継続する必要があるのか、厳しく精査する。多くのSPAはセッションベースで動作するため、ブラウザを閉じたら再度認証でも問題ないケースも多い。
    • もし refresh_token が絶対に必要な場合は、サーバーサイド(Confidential Client)で安全に管理し、SPAからは直接アクセスさせない構成を推奨する。
  • Confidential Client (サーバーサイドアプリケーション) の場合:
    • Authorization Code Flow を利用し、client_secret をサーバーサイドで厳重に管理する。
    • offline_access スコープは、システム連携の要件(バッチ処理など)で長期的なアクセスが必要な場合にのみ検討する。

当時は、OAuth 2.0のフローが多すぎて、どれを選べばいいか混乱していた部分もあった。教科書的な解説では、各フローの目的や特性は示されているものの、具体的なアプリケーションのアーキテクチャとクライアントタイプの関連性を、自分のケースに当てはめて深く考察できていなかったのだ。

特に、SalesforceのConnected App設定とOAuth 2.0の仕様の関係性について、もっと深く理解しておくべきだったと痛感している。Public ClientとConfidential Clientの区別、そしてそれに応じたセキュリティメカニズム(特にPKCE)の重要性は、身をもって学んだ教訓だ。

これは当時の自分向けのメモだ。

コメント