Android の AccountManager は、異なる UID を持つアプリに対してキャッシュされた同じ認証トークンをフェッチするように見えます - これは安全ですか? アクセス トークンは異なるクライアント間で共有されることが想定されていないため、OAuth2 との互換性はないようです。
背景/コンテキスト
OAuth2プロバイダーであるサーバーへのREST APIリクエストの認証/承認にOAuth2を使用するAndroidアプリを構築しています。このアプリは (サードパーティのアプリではなく) 「公式」アプリであるため、信頼できる OAuth2 クライアントと見なされます。そのため、OAuth2 トークンを取得するためにリソース所有者のパスワード フローを使用しています: ユーザー (リソース所有者)ユーザー名/パスワードをアプリに入力すると、クライアント ID とクライアント シークレットがユーザー資格情報と共にサーバーの OAuth2 トークン エンドポイントに送信され、API 呼び出しに使用できるアクセス トークンと long-有効期限が切れたときに新しいアクセス トークンを取得するために使用されるライブ リフレッシュ トークン。これは、ユーザーのパスワードよりも更新トークンをデバイスに保存する方が安全だからです。
デバイスでアカウントと関連するアクセス トークンを管理するためにAccountManagerを利用しています。私は独自の OAuth2 プロバイダーを提供しているので、この Android 開発ガイドで説明され、SampleSyncAdapter サンプル プロジェクトで示されているように、 AbstractAccountAuthenticatorとその他の必要なコンポーネントを拡張して、独自のカスタム アカウント タイプを作成しました。アプリ内からカスタム タイプのアカウントを正常に追加し、[アカウントと同期] Android 設定画面からそれらを管理できます。
問題
ただし、AccountManager が認証トークンをキャッシュして発行する方法に関心があります。具体的には、特定のアカウント タイプとトークン タイプの同じ認証トークンが、ユーザーがアクセスを許可したすべてのアプリからアクセスできるように見えることです。
AccountManager を介して認証トークンを取得するには、 AccountManager.getAuthToken() を呼び出して、認証トークンを取得するAccountインスタンスと目的のauthTokenType
. 指定されたアカウントと authTokenType の認証トークンが存在し、ユーザーが認証トークン要求を行ったアプリにアクセスを許可した場合 ( 「アクセス要求の許可」画面を介して) (要求元のアプリの UID が一致しない場合など)オーセンティケーターの UID)、トークンが返されます。私の説明が不足している場合は、この役立つブログ エントリで非常に明確に説明されています。その投稿に基づいて、AccountManagerとAccountManagerServiceのソースを調べた後(AccountManager の面倒な作業を行う内部クラス) 私自身の場合、authTokenType/account コンボごとに 1 つの認証トークンのみが保存されているようです。
したがって、悪意のあるアプリが認証システムで使用されているアカウントの種類と authTokenType を知っていた場合、ユーザーが悪意のあるアプリにアクセスを許可したと仮定して、AccountManager.getAuthToken() を呼び出して、アプリに保存されている OAuth2 トークンへのアクセスを取得できる可能性があります。アプリ。
私にとっての問題は、AccountManager のデフォルトのキャッシュ実装がパラダイムに基づいて構築されていることです。OAuth2 認証/承認コンテキストをレイヤー化すると、電話/デバイスがサービス/リソース プロバイダーの単一の OAuth2 クライアントであると見なされます。 . 一方、私にとって理にかなっているパラダイムは、各アプリ/UID を独自の OAuth2 クライアントと見なす必要があるということです。OAuth2 プロバイダーがアクセス トークンを発行するとき、デバイス上のすべてのアプリではなく、正しいクライアント ID とクライアント シークレットを送信した特定のアプリのアクセス トークンを発行しています。たとえば、ユーザーは私の公式アプリ (アプリ クライアント A と呼びます) と、私の API を使用する "ライセンスされた" サードパーティ アプリ (アプリ クライアント B と呼びます) の両方をインストールしている可能性があります。公式のクライアント A の場合、私の OAuth2 プロバイダーは、API のパブリック部分とプライベート部分の両方へのアクセスを許可する「スーパー」タイプ/スコープ トークンを発行する場合がありますが、サードパーティのクライアント B の場合、私のプロバイダーは「制限付き」タイプを発行する場合があります。パブリック API 呼び出しへのアクセスのみを許可する /scope トークン。アプリ クライアント B がアプリ クライアント A のアクセス トークンを取得することはできません。ユーザーがクライアント A のスーパー トークンに対してクライアント B に承認を付与したとしても、私の OAuth2 プロバイダーはそのトークンをクライアント A に付与することのみを意図していたという事実は残ります。
ここで何かを見落としていますか?アプリごと/UIDベース(各アプリは個別のクライアント)で認証トークンを発行する必要があるという私の考えは合理的/実用的ですか、それともデバイスごとの認証トークン(各デバイスはクライアントです)が標準/承認されていますか練習?
または、 / に関するコード/セキュリティ制限の理解に何らかの欠陥があるため、AccountManager
このAccountManagerService
脆弱性は実際には存在しませんか? 上記のクライアント A/クライアント B のシナリオをAccountManager
とカスタム オーセンティケーターでテストしたところ、異なるパッケージ スコープと UID を持つテスト クライアント アプリ B は、サーバーがテスト用に発行した認証トークンを取得できました。クライアントアプリAを同じものに渡しますauthTokenType
(その間、「アクセスリクエスト」許可画面でプロンプトが表示されましたが、私はユーザーなので無知なので承認しました)...
可能な解決策
を。"Secret" authTokenType
認証トークンを取得するには、 を知っているauthTokenType
必要があります。authTokenType
特定のシークレットトークンタイプに対して発行されたトークンが、シークレットトークンタイプを知っている「承認された」クライアントアプリのみによって取得されるように、クライアントシークレットのタイプとして扱われるべきですか? これはあまり安全ではないようです。ルート化されたデバイスでは、システムのテーブルのauth_token_type
列を調べることができますauthtokens
accounts
データベースにアクセスし、トークンと共に保存されている authTokenType 値を調べます。したがって、私のアプリ (およびデバイスで使用される承認済みのサードパーティ アプリ) のすべてのインストールで使用される「秘密の」認証トークン タイプは、1 つの中央の場所で公開されます。少なくとも OAuth2 クライアント ID/シークレットを使用すると、アプリと一緒にパッケージ化する必要がある場合でも、それらはさまざまなクライアント アプリに分散されており、それらを難読化する試みが行われる可能性があります (何もないよりはましです)。アプリをアンパッケージ/逆コンパイルします。
b. カスタム認証トークンAccountManager.KEY_CALLER_UIDとAuthenticatorDescription.customTokens
のドキュメント、および以前に参照したソース コードによると、カスタム アカウント タイプが「カスタム トークン」を使用することを指定し、独自のトークン キャッシング/ストレージの実装を内部でスピンできるはずです。私のカスタムオーセンティケーター。UIDごとに認証トークンを保存/取得するために、呼び出し元アプリのUIDを取得できます。基本的に、追加されることを除いて、デフォルトの実装のようなテーブルがありますAccountManagerService
authtokens
uid
column so that tokens are uniquely indexed on U̲I̲D̲, a̲c̲c̲o̲u̲n̲t̲, and A̲u̲t̲h̲ ̲T̲o̲k̲e̲n̲ ̲T̲y̲p̲e̲ (as opposed to just a̲c̲c̲o̲u̲n̲t̲ and A̲u̲t̲h̲ ̲T̲o̲k̲e̲n̲ ̲T̲y̲p̲e̲). これは、「秘密の」authTokenTypes を使用するよりも安全なソリューションのようです。これはauthTokenTypes
、アプリ/認証システムのすべてのインストールで同じものを使用する必要があるためです。一方、UID はシステムごとに異なり、簡単にスプーフィングすることはできません。独自のトークン キャッシング メカニズムを作成して管理するための嬉しいオーバーヘッドは別として、セキュリティの観点から、このアプローチにはどのような欠点がありますか? やり過ぎですか?私は本当に何かを保護しているのでしょうか、それとも、そのような実装が整っていても、ある悪意のあるアプリ クライアントが別のアプリ クライアントを簡単に取得できるような何かが欠けているのでしょうか。AccountManager
authTokenType
(s) 秘密であることが保証されていない (前述の悪意のあるアプリが OAuth2 クライアント シークレットを知らないため、新しいトークンを直接取得することはできずAccountManager
、承認されたユーザーに代わって既にキャッシュされているトークンを取得することしか期待できないと仮定します)アプリクライアント)?
c. OAuth2 トークン
を使用してクライアント ID/シークレットを送信するAccountManagerService
のデフォルトのトークン ストレージ実装を使用し、アプリの認証トークンへの不正アクセスの可能性を受け入れることもできますが、API リクエストに OAuth2 クライアント ID とクライアント シークレットを常に含めるように強制することもできます。アクセストークンに加えて、アプリが最初にトークンが発行された承認されたクライアントであることをサーバー側で確認します。ただし、これは避けたいと思います。なぜなら、A)私の知る限り、OAuth2 仕様では、保護されたリソース要求に対してクライアント認証は必要ありません。アクセス トークンのみが必要であり、B)毎回クライアントを認証する追加のオーバーヘッドを回避したいからです。リクエスト。
これは一般的なケースでは不可能です (サーバーが取得するのは、プロトコル内の一連のメッセージだけです。これらのメッセージを生成したコードを特定することはできません)。--マイケル
しかし、クライアントが最初にアクセス トークンを発行する OAuth2 フローの最初のクライアント認証についても同じことが言えます。唯一の違いは、トークン要求だけで認証するのではなく、保護されたリソースの要求も同じ方法で認証されることです。(クライアント アプリは、その c̲l̲i̲e̲n̲t̲ ̲i̲d̲ と c̲l̲i̲e̲n̲t̲ ̲s̲e̲c̲r̲e̲t̲ のloginOptions
パラメーターを介して渡すことができることに注意してくださいAccountManager.getAuthToken()
。これは、OAuth2 プロトコルに従って、私のカスタム オーセンティケーターがリソース プロバイダーに渡すだけです)。
主な質問
- 同じ authTokenType で AccountManager.getAuthToken() を呼び出すことにより、あるアプリが別のアプリのアカウントの authToken を取得することは本当に可能ですか?
これが可能である場合、これはOAuth2 コンテキスト内で有効/実用的なセキュリティ上の問題ですか?
ユーザーに与えられた認証トークンがそのユーザーからの秘密のままであることに決して依存することはできません...そのため、Android がその設計のあいまいさの目標によってこのセキュリティを無視することは合理的です-Michael
BUT - ユーザー (リソース所有者) が私の同意なしに認証トークンを取得することについては心配していません。不正なクライアントが心配です(アプリ)。ユーザーが自分の保護されたリソースの攻撃者になりたい場合は、自分自身をノックアウトできます。私は、ユーザーが私のクライアント アプリをインストールし、無意識のうちに、正しい authTokenType を渡したという理由だけでアプリの認証トークンにアクセスできる「なりすまし」クライアント アプリをインストールすることは可能であってはならないと言っています。アクセス要求画面を調べるのが怠惰/無知/急いでいる。この類推は少し単純化しすぎているかもしれませんが、インストールされている Facebook アプリが Gmail アプリによってキャッシュされたメールを読み取れないことは「あいまいさによるセキュリティ」とは考えていません。これは、私 (ユーザー) が電話をルート化してキャッシュを調べることとは異なります。内容は自分。
ユーザーは、アプリがあなたのトークンを使用するための (Android システムが提供する) アクセス要求を受け入れる必要がありました...それを考えると、Android ソリューションは問題ないようです - アプリは、尋ねることなくユーザーの認証を黙って使用することはできません --マイケル
しかし、これは承認の問題でもあります。「公式」クライアントに対して発行された認証トークンは、そのクライアントとそのクライアントのみが承認される保護されたリソースのセットへの鍵です。ユーザーはこれらの保護されたリソースの所有者であるため、サードパーティのクライアント (「承認された」パートナー アプリまたはフィッシャー) からのアクセス要求を受け入れる場合、事実上、サードパーティを承認していると主張できると思います。それらのリソースへのアクセスを要求したパーティ クライアント。しかし、私はこれに問題があります:
- 平均的なユーザーは、この決定を適切に行うことができるほどセキュリティを意識していません。Android のアクセス要求画面で「拒否」をタップするのは、ユーザーの判断だけに頼って、下手なフィッシング攻撃でも防げるとは思いません。ユーザーにアクセス要求が提示されると、私のオーセンティケーターは非常に詳細になり、ユーザーが要求を受け入れた場合に許可する機密保護されたリソースのすべてのタイプ (クライアントのみがアクセスできるようにする必要があります) を列挙することができます。そしてほとんどの場合、ユーザーはまだあまりにも無知であり、受け入れようとしています. また、他のより巧妙なフィッシングの試みでは、「なりすまし」アプリは、ユーザーがアクセス要求画面で眉をひそめるにはあまりにも「公式」に見えるだけです。か、ここ」「このリクエストを受け入れないでください。この画面が表示されている場合は、悪意のあるアプリがあなたのアカウントにアクセスしようとしています!」このような場合、ほとんどのユーザーがリクエストを拒否することを願っています。しかし、なぜそこまでする必要があるのでしょうか。Android が認証トークンを発行対象の各アプリ/UID の範囲に分離したままにしておくだけであれば、これは問題になりません。単純化しましょう - 私が「公式」クライアント アプリを 1 つしか持っていない場合でも、リソース プロバイダーは他のサードパーティ クライアントにトークンを発行することさえ気にしません。 AccountManager、「いいえ。この認証トークンをロックダウンして、私のアプリだけがアクセスできるようにしてください。」「カスタム トークン」ルートを使用すればこれを行うことができますが、その場合でも、ユーザーに最初にアクセス リクエスト画面が表示されるのを防ぐことはできません。少なくとも、
- Android のドキュメントでさえ、OAuth2を認証 (およびおそらく承認)の「業界標準」として認識しています。OAuth2 仕様では、アクセス トークンをクライアント間で共有したり、いかなる方法でも漏らしたりしてはならないと明確に述べています。では、デフォルトの AccountManager の実装/構成により、クライアントは、別のクライアントがサービスから最初に取得した同じキャッシュされた認証トークンを簡単に取得できるのはなぜでしょうか? AccountManager 内での簡単な修正は、キャッシュされたトークンをサービスから最初に取得したときと同じアプリ/UID に対してのみ再利用することです。特定の UID で使用できるローカルにキャッシュされた認証トークンがない場合は、サービスから取得する必要があります。または、少なくともこれを開発者向けの構成可能なオプションにします。
- OAuth 3-legged フロー (ユーザーがクライアントへのアクセスを許可することを含む) では、A)クライアントを認証し、B )クライアントが有効な場合、ユーザーにアクセス許可要求を提示しますか? Android が (誤って) 流れの中でこの役割を奪っているようです。
しかし、ユーザーはアプリが以前の認証をサービスに再利用することを明示的に許可できます。これはユーザーにとって便利です.-- Michael
しかし、利便性の ROI がセキュリティ リスクを正当化するとは思いません。ユーザーのパスワードがユーザーのアカウントに保存されている場合、実際にユーザーが購入できる唯一の利便性は、Web リクエストをサービスに送信して、実際に承認された新しい個別のトークンを取得することです。要求しているクライアントに対して、クライアントに対して承認されていないローカルにキャッシュされたトークンが返されます。そのため、ユーザーは、リソースが盗まれたり悪用されたりしてユーザーが大きな不便を被るリスクを冒して、「サインイン...」進行状況ダイアログを数秒短く表示するというわずかな利便性を得ることができます。
A) OAuth2 プロトコルを使用して API リクエストを保護すること、B)独自の OAuth2 リソース/認証プロバイダーを提供すること (たとえば、Google や Facebook で認証するのではなく)、およびC) Android の AccountManager を利用してカスタムアカウントタイプとそのトークンを管理する場合、提案されたソリューションは有効ですか? どれが最も理にかなっていますか? 長所/短所を見落としていますか? 私が考えたことのない価値のある代替手段はありますか?
[用途] 代替クライアント公式クライアントのみがアクセスできるようにする秘密の API はありません。人々はこれを回避します。ユーザーが使用している (将来の) クライアントに関係なく、すべての公開 API が安全であることを確認してください -- Michael
しかし、これはそもそも OAuth2 を使用する主な目的の 1 つを無効にしないでしょうか? すべての潜在的な被承認者が保護されたリソースの同じスコープに対して承認される場合、承認は何の役に立つでしょうか?
これが問題だと感じた人はいますか? また、どのように回避しましたか? 他の人がこれをセキュリティの問題/懸念であると感じているかどうかを調べるために、大規模なグーグル検索を行いましたが、Android の AccountManager と認証トークンに関連するほとんどの投稿/質問は、Google アカウントで認証する方法に関するものであり、そうではないようですカスタム アカウント タイプと OAuth2 プロバイダーを使用します。さらに、同じ認証トークンが異なるアプリで使用される可能性について懸念している人を見つけることができませんでした。これは、そもそもこれが本当に可能性があるかどうか、または懸念に値するかどうか疑問に思います (私の最初の 2 つの「重要な質問」を参照してください)。 」に記載されています)。
ご意見/ご指導をよろしくお願いいたします。
に応答して...
マイケルの答え- あなたの答えで私が抱えている主な問題は次のとおりです。
ユーザー/電話/デバイス自体が1つの「大きな」クライアントであるのとは対照的に、アプリをサービスの個別の個別のクライアントと考える傾向があるため、1つのアプリに対して承認されたトークンはそうすべきではありません。デフォルト、持っていないものに譲渡できます。次の可能性があるため、各アプリを個別のクライアントと見なすことは意味がないとほのめかしているようです。
ユーザーがルート化された電話を実行している可能性があり、トークンを読み取り、プライベート API へのアクセスを取得する可能性があります... [または] ユーザーのシステムが侵害された場合 (この場合、攻撃者はトークンを読み取ることができます)
したがって、デバイス自体のアプリ間のセキュリティを保証することはできないため、大まかに言えば、デバイスをサービスのクライアントと見なす必要があります。システム自体が侵害された場合、そのデバイスからサービスに送信されるリクエストを認証/承認する保証はありません。しかし、TLS についても同じことが言えます。エンドポイント自体を保護できない場合、トランスポート セキュリティは関係ありません。また、侵害されていない大多数の Android デバイスでは、同じ認証トークンを共有してすべてを 1 つにまとめるのではなく、各アプリ クライアントを個別のエンドポイントと見なす方が安全であると考えています。
- 「アクセス要求」画面が表示された場合 (同意してインストールする前に常によく読んでいるソフトウェアの使用許諾契約に似ています)、悪意のある/承認されていないクライアント アプリとそうでないものを区別するユーザーの判断を信用できません。