AndroidのAccountManagerは、異なるUIDを持つアプリに対して同じキャッシュされた認証トークンをフェッチするようです-これは安全ですか?アクセストークンは異なるクライアント間で共有されることを想定していないため、OAuth2との互換性はありません。
Android RESTの認証/承認にOAuth2を使用するアプリを作成しています。これは、OAuth2プロバイダーである私のサーバーへのAPIリクエストです。アプリは「サードパーティのアプリではなく」「公式」アプリは、信頼できるOAuth2クライアントと見なされるため、OAuth2トークンを取得するためにリソース所有者のパスワードフローを使用しています。ユーザー(リソース所有者)がユーザー名/パスワードを入力しますアプリに送信します。次に、API呼び出しを行うために使用できるアクセストークンの代わりに、クライアントIDとクライアントシークレットをユーザー認証情報とともにサーバーのOAuth2トークンエンドポイントに送信します。有効期限が切れたときに新しいアクセストークンを取得する理由は、更新トークンをデバイスに保存する方がユーザーのパスワードよりも安全であることです。
私は AccountManager を使用して、デバイス上のアカウントと関連するアクセストークンを管理しています。私は独自のOAuth2プロバイダーを提供しているので、説明に従って AbstractAccountAuthenticator およびその他の必要なコンポーネントを拡張することにより、独自のカスタムアカウントタイプを作成しました このAndroid開発ガイド とSampleSyncAdapterサンプルプロジェクトで示されています。アプリ内からカスタムタイプのアカウントを正常に追加し、「アカウントと同期」からアカウントを管理できますAndroid設定画面。
ただし、私はAccountManagerが認証トークンをキャッシュして発行する方法に関心があります。具体的には、特定のアカウントタイプとトークンタイプの同じ認証トークンは、ユーザーがアクセスを許可しました。
AccountManagerを介して認証トークンを取得するには、 AccountManager.getAuthToken() を呼び出して、認証トークンを取得する Account インスタンスを渡し、必要なauthTokenType
。指定されたアカウントとauthTokenTypeに認証トークンが存在し、ユーザーが認証トークンリクエストを行ったアプリに(grant "Access Request" screenを介して)アクセスを許可した場合(このような場合)リクエストしているアプリのUIDがオーセンティケーターのUIDと一致しない場合)、トークンが返されます。私の説明が足りない場合は、 この役立つブログエントリ で非常に明確に説明しています。その投稿に基づいて、そして AccountManager と AccountManagerService (AccountManagerの重い作業を行う内部クラス)のソースを調べた後、authTokenType /アカウントの組み合わせごとに1つの認証トークンのみが保存されます。
したがって、認証システムが使用するアカウントタイプとauthTokenType(s)を悪意のあるアプリが知っていた場合、それはAccountManager.getAuthToken()を呼び出して、アプリの保存されたOAuth2トークンへのアクセスを取得できます。ユーザーが悪意のあるアプリへのアクセスを許可すると仮定します。
私にとっての問題は、AccountManagerのデフォルトのキャッシング実装がパラダイムに基づいて構築されていることです。OAuth2認証/承認コンテキストを階層化すると、電話/デバイスがサービス/リソースプロバイダーの単一のOAuth2クライアントであると見なされます。 。一方、私にとって意味のあるパラダイムは、各app/UIDが独自のOAuth2クライアントと見なされることです。私のOAuth2プロバイダーがアクセストークンを発行すると、それが発行されますデバイス上のすべてのアプリではなく、正しいクライアントIDとクライアントシークレットを送信した特定のアプリのアクセストークン。たとえば、ユーザーは私の公式アプリ(アプリクライアントAと呼びます)と、私のAPI(アプリクライアントBと呼びます)を使用する「ライセンス済み」のサードパーティアプリをインストールしている可能性があります。公式のクライアントAの場合、OAuth2プロバイダーは、APIのパブリック部分とプライベート部分の両方へのアクセスを許可する「スーパー」タイプ/スコープトークンを発行できますが、サードパーティのクライアントBの場合、プロバイダーは「制限付き」タイプを発行できますパブリックAPI呼び出しへのアクセスのみを許可する/ scopeトークン。アプリクライアントBがアプリクライアントAのアクセストークンを取得できないようにする必要があります。これは、現在のAccountManager/AccountManagerService実装で許可されているようです。ユーザーが承認を付与した場合でもクライアントAのスーパートークン用のクライアントBに対して、私のOAuth2プロバイダーはそのトークンをクライアントAに付与することのみを意図していたという事実は変わりません。
ここで何か見落としているのですか?認証トークンはアプリ/ UIDベース(各アプリは個別のクライアントです)で発行する必要があると私は信じていますか、またはデバイスごとの認証トークン(各デバイスはクライアントです)は標準/承認済みです練習?
または、この脆弱性が実際には存在しないような、AccountManager
/AccountManagerService
に関するコード/セキュリティ制限の私の理解にいくつかの欠陥がありますか?上記のクライアントA /クライアントBのシナリオをAccountManager
とカスタム認証システムを使用してテストしましたが、パッケージのスコープとUIDが異なるテストクライアントアプリBは、サーバーが発行した認証トークンを取得できました同じauthTokenType
を渡してクライアントアプリAをテストします(その間、「アクセスリクエスト」付与画面が表示され、ユーザーであるので無知なので、これを承認しました)...
a。 「秘密の」authTokenType
認証トークンを取得するには、 authTokenType
が必要です。 authTokenType
をクライアントシークレットのタイプとして処理する必要があります。これにより、特定のシークレットトークンタイプに対して発行されたトークンは、シークレットトークンタイプを知っている「承認された」クライアントアプリのみが取得できます。これは非常に安全ではないようです。ルート権限を取得されたデバイスでは、システムのauthtokens
データベースのaccounts
テーブルの_auth_token_type
_列を調べて、トークンとともに保存されているauthTokenType値を調べることができます。したがって、私のアプリのすべてのインストールで使用される「秘密の」認証トークンタイプ(およびデバイスで使用される承認済みのサードパーティアプリ)は、1つの中央の場所で公開されます。少なくともOAuth2クライアントID /シークレットを使用すると、アプリと一緒にパッケージ化する必要がある場合でも、それらはさまざまなクライアントアプリに分散され、それらを難読化して(何もないよりはましです)、そうする可能性のあるユーザーを阻止するのに役立ちますアプリをアンパッケージ/逆コンパイルします。
b。カスタム認証トークン
AccountManager.KEY_CALLER_UID および AuthenticatorDescription.customTokens のドキュメント、および以前に参照したAccountManagerService
ソースコードに従って、カスタムを指定できるようにする必要がありますアカウントタイプは「カスタムトークン」を使用し、カスタム認証システム内で独自のトークンキャッシング/ストレージ実装をスピンします。ここで、呼び出し元アプリのUIDを取得して、UIDごとに認証トークンをストア/フェッチできます。そのトークンが一意にUID、アカウント、および認証トークンタイプ(ジャストアカウントおよび認証トークンタイプではなく)にインデックス付けされているので、追加authtokens
列があるだろう除き基本的に、私は、デフォルトの実装などのuid
テーブルを持っているでしょう。これは、「秘密の」authTokenTypesを使用するよりも安全なソリューションのようです。これは、私のアプリ/認証システムのすべてのインストールで同じauthTokenTypes
を使用する必要があるためです。一方、UIDはシステムによって異なり、簡単に偽装することはできません。独自のトークンキャッシングメカニズムを記述および管理するための楽しいオーバーヘッドの他に、このアプローチにはセキュリティの面でどのような欠点がありますか?やりすぎですか?私は本当に何かを保護していますか、それともそのような実装があったとしても、悪意のあるアプリクライアントがAccountManager
とauthTokenType
(s)を使用して別のアプリクライアントの認証トークンを取得するのは簡単です。シークレットであることが保証されています(悪意のあるアプリがOAuth2クライアントシークレットを認識していないため、直接新しいトークンを取得できず、承認済みのアプリクライアントに代わってAccountManager
にすでにキャッシュされているトークンのみを取得できると想定しています)?
c。クライアントID /シークレットとOAuth2トークンを送信AccountManagerService
のデフォルトのトークンストレージ実装をそのまま使用して、アプリの認証トークンへの不正アクセスの可能性を受け入れることができますが、アクセスに加えて、APIリクエストにOAuth2クライアントIDとクライアントシークレットを常に含めるように強制できますトークンを確認し、最初にトークンが発行された承認済みクライアントがアプリであることをサーバー側で確認します。ただし、A)AFAIKの場合、これを回避したいと思います。OAuth2仕様では、保護されたリソース要求に対してクライアント認証は必要ありません。アクセストークンのみが必要で、B)各リクエストでクライアントを認証する追加のオーバーヘッドを回避したいと思います。
これは一般的なケースでは不可能です(サーバーが取得するのはプロトコル内の一連のメッセージだけです。これらのメッセージを生成したコードは特定できません)。 -- Michael
しかし、クライアントが最初にアクセストークンを発行するOAuth2フローでの初期クライアント認証についても同じことが言えます。唯一の違いは、トークンリクエストだけで認証するのではなく、保護されたリソースのリクエストも同じ方法で認証されることです。 (クライアントアプリは、AccountManager.getAuthToken()
のloginOptions
パラメーターを介してc parameterl̲i̲e̲n̲t̲ ̲i̲d̲およびc̲l̲i̲e̲n̲t̲ ̲s̲e̲c̲r̲e̲t̲を渡すことができることに注意してください。これは、カスタム認証システムがO2ごとにリソースプロバイダーに渡すだけです。
これが可能である場合、これはOAuth2コンテキスト内の有効/実用的なセキュリティ問題ですか?
ユーザーに与えられた認証トークンに依存することはできません。そのユーザーからの秘密が残っているため、Androidが設計のあいまいな目標によってこのセキュリティを無視することは妥当です-- マイケル
[〜#〜] but [〜#〜]-ユーザー(リソース所有者)が私の同意なしに認証トークンを取得することについて心配していません。 不正なクライアント(アプリ)が心配です。ユーザーが自分の保護されたリソースの攻撃者になりたい場合は、自分をノックアウトできます。ユーザーが自分のクライアントアプリをインストールすることはできません。また、意図せずに、正しいauthTokenTypeを渡し、ユーザーがアクセス要求画面を調べるのに怠惰/気づかない/急いでいる。この例えは少し単純化しすぎているかもしれませんが、私がインストールしたFacebookアプリがGmailアプリでキャッシュされたメールを読み取れないことは、「あいまいさによるセキュリティ」だとは考えていません。自分自身。
ユーザーは、アプリがトークンを使用するために(Androidシステムが提供する)アクセス要求を受け入れる必要がありました...その場合、AndroidソリューションはOKのようです-アプリはユーザーの認証を黙って使用できません尋ねずに-- Michael
[〜#〜] but [〜#〜]-これもauthorizationの問題です-認証トークンが発行されました私の「公式」クライアントは、そのクライアントとそのクライアントのみが許可されている保護されたリソースのセットの鍵です。ユーザーはこれらの保護されたリソースの所有者であるため、サードパーティのクライアントからのアクセス要求を受け入れると(「アクションを実行した」パートナーアプリやフィッシャーなど)、事実上、サードパーティを承認していると主張できます。これらのリソースへのアクセスを要求したパーティクライアント。しかし、これには問題があります:
ただし、ユーザーはアプリがサービスに対する以前の認証を再利用することを明示的に許可できます。これはユーザーにとって便利です。-- Michael
[〜#〜]しかし[〜#〜]-利便性のROIがセキュリティリスクを保証するものではないと思います。ユーザーのパスワードがユーザーのアカウントに保存されている場合、実際にユーザーに購入できるのは、実際に許可されている新しい個別のトークンを取得するためにWebリクエストをサービスに送信する代わりに、要求元のクライアントには、クライアントに許可されていないローカルにキャッシュされたトークンが返されます。そのため、ユーザーは、「サインイン中...」の進行状況ダイアログが数秒少なく表示されるというわずかな利便性を得ますが、リソースが盗まれたり誤用されたりすることによってユーザーに大きな不便が生じるリスクがあります。
APIリクエストを保護するためにOAuth2プロトコルを使用してA)にコミットしていることを念頭に置いて、B)独自のOAuth2リソース/認証プロバイダー(たとえば、GoogleまたはFacebookでの認証とは対照的)を提供し、C)AndroidのAccountManagerを使用してカスタムアカウントタイプとそのトークンを管理する(s)、私の提案するソリューションのいずれかは有効ですか?どちらが最も理にかなっていますか?長所/短所を見落としているのですか?私が考えていない価値のある代替案はありますか?
[使用]代替クライアント公式クライアントのみがアクセスできるようにしようとする秘密のAPIはありません。人々はこれを回避します。ユーザーが使用している(将来の)クライアントに関係なく、公開されているすべてのAPIが安全であることを確認します-- Michael
[〜#〜] but [〜#〜]-これは、最初にOAuth2を使用する主要な目的の1つを無効にしませんか?可能性のあるすべての被許可者が保護されたリソースの同じスコープに対して承認される場合、承認はどのように役立ちますか
これが問題であると他の誰かが感じましたか、それをどのように回避しましたか?他の人がこれをセキュリティの問題/懸念ですが、AndroidのAccountManagerおよび認証トークンに関するほとんどの投稿/質問は、カスタムアカウントタイプやOAuth2プロバイダーではなく、Googleアカウントで認証する方法に関するものであるようです。さらに、同じ認証トークンが別のアプリで使用されている可能性について懸念を抱いている人を見つけることができなかったため、そもそもこれが本当に可能性/懸念に値するかどうか疑問に思う(最初の2つの「主要な質問」を参照) 」を参照)。
私はあなたの入力/ガイダンスに感謝します!
Michael's Answer-私があなたの答えで抱えている主な困難は次のとおりです。
ユーザー/電話/デバイス自体が1つの「ビッグ」クライアントであるのとは対照的に、アプリはサービスの個別の別個のクライアントであると考える傾向があります。そのため、1つのアプリに対して承認されたトークンは、デフォルトでは、転送できないものに転送できます。次のような可能性があるため、各アプリを個別のクライアントと見なすのは無意味であるとほのめかしているようです。
ユーザーがroot化された電話を実行し、トークンを読み取ってプライベートAPIにアクセスする可能性があります... [または]ユーザーのシステムが侵害された場合(攻撃者はこの場合、トークンを読み取る可能性があります)
つまり、デバイス自体のアプリ間のセキュリティを保証することはできないため、大まかな計画では、デバイスをサービスのクライアントと見なす必要があります。システム自体が危険にさらされている場合は、そのデバイスからサービスに送信される認証/承認リクエストの保証はありません。しかし、TLSなどについても同じことが言えます。エンドポイント自体を保護できない場合、トランスポートセキュリティは重要ではありません。 Androidデバイスは侵害されていないため、同じように共有してすべてを1つにまとめるよりも、各アプリクライアントを個別のエンドポイントと見なす方が安全です。認証トークン。
公式クライアントAの場合、私のOAuth2プロバイダーは、私のAPIのパブリック部分とプライベート部分の両方へのアクセスを許可する「スーパー」タイプ/スコープトークンを発行する場合があります
一般的なケースでは、ユーザーに与えられた認証トークンにシークレットそのユーザーからを残して信頼することはできません。たとえば、ユーザーはroot化された電話を実行していて、トークンを読み取って、プライベートAPIにアクセスできます。ユーザーのシステムが危険にさらされた場合も同様です(この場合、攻撃者がトークンを読み取る可能性があります)。
別の言い方をすれば、認証されたユーザーが同時にアクセスできる「プライベート」APIなどは存在しないため、Androidが設計のあいまいな目標によってこのセキュリティを無視することは合理的です。
悪意のあるアプリ...アプリに保存されているOAuth2トークンへのアクセスを取得する可能性があります
悪意のあるアプリのケースでは、Androidの権限システムが悪意のあるアプリの分離を提供することが期待されているため、悪意のあるアプリがクライアントのトークンを使用できないようにすることがより合理的に聞こえ始めます(ユーザーが権限を読み取る/注意することを前提とする場合)彼らがそれをインストールしたときに受け入れられた)。ただし、あなたが言うように、ユーザーはアプリがトークンを使用するために(Androidシステムが提供する)アクセス要求を受け入れる必要がありました。
その場合、Androidソリューションは問題ないようです。アプリはユーザーの認証を要求せずに黙って使用することはできませんが、ユーザーはアプリがサービスに対する以前の認証を再利用することを明示的に許可できます。ユーザーにとって便利です。
「秘密」のauthTokenType ...は非常に安全ではないようです
同意-それはあいまいさによるセキュリティのもう1つの層にすぎません。認証を共有したいアプリはとにかくauthTokenTypeが何であるかを調べなければならなかったように思われるので、このアプローチを採用すると、この架空のアプリ開発者にとって少し扱いにくくなります。
OAuth2トークンを使用してクライアントID /シークレットを送信...サーバー側でアプリが承認済みクライアントであることを確認します
これは一般的なケースでは不可能です(サーバーが取得するのはプロトコル内の一連のメッセージだけです。これらのメッセージを生成したコードは特定できません)。この特定の例では、(非ルート)代替クライアント/悪意のあるアプリのより限定的な脅威から保護する可能性があります-私はAccountManagerについて十分に詳しくないので、コメントしません(カスタム認証トークンソリューションについては同じです)。
2つの脅威について説明しました。ユーザーがアカウントにアクセスしたくない悪意のあるアプリと、API(開発者)がAPIの一部を使用したくない代替クライアントです。
悪意のあるアプリ:提供しているサービスの機密性を考慮してください。 Google/Twitterアカウントは、Androidの保護機能(インストールの許可、アクセス要求画面)に依存しています。 isより機密性が高い場合は、AndroidのAccountManagerを利用するという制約が適切かどうかを検討してください。アカウントの悪用からユーザーを強力に保護するには、危険なアクション(オンラインバンキングでの新しい受信者のアカウント詳細の追加など)に対する2要素認証を試みます。
代替クライアント:公式クライアントのみがアクセスできるようにするための秘密のAPIはありません。人々はこれを回避します。ユーザーが使用している(将来の)クライアントに関係なく、公開されているすべてのAPIが安全であることを確認します。
あなたの観察は正しいです。認証システムは、インストールするアプリと同じUIDで実行されます。別のアプリがアカウントマネージャーに接続し、この認証システムのトークンを取得すると、提供された認証システムサービスにバインドされます。 UIDとして実行されるため、新しいアカウントはこの認証システムに関連付けられます。アプリがgetAuthTokenを呼び出すと、バインディングが発生し、Authenticatorは引き続き同じUIdで実行されます。デフォルトの組み込み権限はアカウントのUIDをチェックするため、異なる認証システムは異なる認証システムから別のアカウントにアクセスできませんでした。
この問題は、アカウントマネージャーサービスがバンドルに追加するため、addAccountおよびGetAuthTokenに「Calling UID」を使用することで解決できます。オーセンティケーター実装はそれをチェックできます。
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) throws NetworkErrorException {
Log.v(
TAG,
"getAuthToken() for accountType:" + authTokenType + " package:"
+ mContext.getPackageName() + "running pid:" + Binder.getCallingPid()
+ " running uid:" + Binder.getCallingUid() + " caller uid:"
+ loginOptions.getInt(AccountManager.KEY_CALLER_UID));
...
}
他の開発者がそのシークレットを抽出できるため、ネイティブアプリにクライアントシークレットを保存する代わりに、承認フローに従うことをお勧めします。あなたのアプリはウェブアプリではなく、秘密を持つべきではありません。
アカウントを追加するときに、callingUIdにもクエリを実行できます。アプリのUIDとして実行されるaddAccount関連アクティビティでsetUserDataを実行する必要があるため、setUserDataを呼び出すことができます。
getUserDataおよびsetUserDataは組み込みのsqlliteデータベースを使用するため、自分でキャッシュを構築する必要はありません。保存できるのは文字列タイプのみですが、jsonを解析してアカウントごとに追加情報を保存できます。
別のサードパーティアプリがアカウントをクエリし、アカウントでgetAuthtokenを呼び出す場合、アカウントのユーザーデータでUIDを確認できます。呼び出し元のUIDがリストにない場合は、プロンプトやその他の操作を行って許可を得ることができます。許可されている場合は、アカウントに新しいUIDを追加できます。
アプリ間でトークンを共有する:各アプリは通常、異なるクライアントIDで登録されており、トークンを共有するべきではありません。トークンはクライアントアプリ用です。
Storage:AccountManagerはデータを暗号化していません。より安全なソリューションが必要な場合は、トークンを暗号化して保存する必要があります。
アプリでも同じアーキテクチャの問題に直面しています。
私が得た解決策は、oauthトークンを、アプリベンダートークン(たとえば、facebookがアプリに付与するトークン)に関連付け、ハッシュして、デバイスID(Android_id
)。したがって、承認されたアプリのみが、デバイスに対してアカウントマネージャーからのトークンを使用できます。
もちろん、これは新しいセキュリティ層にすぎませんが、完全な証拠にはなりません。
@Michaelが質問に完全に答えたと思います。ただし、答えをよりわかりやすく短くするために、簡単な答えを探している人にはこれを書いています。
Android AccountManager
のセキュリティについてのあなたの懸念は正しいですが、これがOAuthの本来の目的であり、Android AccountManager
は依存します。
つまり、非常に安全な認証メカニズムを探している場合、これは適切なオプションではありません。認証のためにキャッシュされたトークンに依存しないでください。これらのトークンは、不注意に侵入者にアクセス許可を付与したり、ルート権限を取得したデバイスを実行したりするなど、ユーザーのデバイスにセキュリティの脆弱性がある場合に侵入者に簡単に明らかになる可能性があるためです。
オンラインバンキングアプリなどのより安全な認証システムでOAuthのより良い代替策は、公開鍵と秘密鍵を使用して非対称暗号化を使用することです。この場合、ユーザーは使用するたびにパスワードを入力する必要があります。次に、パスワードはデバイスの公開鍵を使用して暗号化され、サーバーに送信されます。ここで、侵入者が暗号化されたパスワードを知ったとしても、その公開鍵でパスワードを復号化することはできないため、侵入者は何もできません。サーバーの秘密鍵のみ。
とにかく、AndroidのAccountManager
システムを利用し、高レベルのセキュリティを維持したい場合は、デバイスにトークンを保存しないことで可能になります。 。getAuthToken
のAbstractAccountAuthenticator
メソッドは、次のようにオーバーライドできます。
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String
authTokenType, Bundle options) throws NetworkErrorException {
AuthenticatorManager authenticatorManager = AuthenticatorManager.authenticatorManager;
Bundle result;
AccountManager accountManager = AccountManager.get(context);
// case 1: access token is available
result = authenticatorManager.getAccessTokenFromCache(account, authTokenType,
accountManager);
if (result != null) {
return result;
}
final String refreshToken = accountManager.getPassword(account);
// case 2: access token is not available but refresh token is
if (refreshToken != null) {
result = authenticatorManager.makeResultBundle(account, refreshToken, null);
return result;
}
// case 3: neither tokens is available but the account exists
if (isAccountAvailable(account, accountManager)) {
result = authenticatorManager.makeResultBundle(account, null, null);
return result;
}
// case 4: account does not exist
return new Bundle();
}
この方法では、account
が存在しても、保存されたトークンがないため、ケース1、ケース2、ケース4のいずれも当てはまりません。したがって、ケース3のみが返され、関連するコールバックで設定して、ユーザーが認証用のユーザー名とパスワードを入力するActivity
を開くことができます。
ここでこれをさらに説明するのが正しいかどうかはわかりませんが、 AccountManager
への私のウェブサイトの投稿は、念のために役立つかもしれません。