ページ

2014/01/11

[Android]TwitterのOAuth認証を行う

Android で Twitter の OAuth って前は WebView で行うのが一般的だった気がします。

でもアプリ内の WebView で実装した場合は、
ユーザー側にはそれが本物なのかフィッシングなのか区別できません。

OAuthの認証にWebViewを使うのはやめよう - Shogo's Blog
アプリ内でWebViewを使うとURLが表示されません. つまり 本当にツイッターにアクセスしているかわからない のです. もし,表示されるのが偽の認証画面だったら,アプリから簡単にパスワードがわかってしまいます.

じゃあ,URL を表示させればいいかというとそういうわけでもありません. 画面上のURL表示なんて簡単に偽装できてしまいます. どんな工夫をしても アプリがパスワードの要求をしていることには変わりありません . アプリはパスワードを簡単に取得できます.

アプリのユーザはTwitterに限らずSNSへのログイン時にブラウザを開かないアプリは信用しないようにしましょう. どこかでパスワードの抜かれている可能性があります. (ただし,公式アプリは除く.公式アプリが信用できないならそもそもサービスを利用できないもんね.)

じゃあ外部のブラウザアプリを開いて、
カスタム URL スキームを Intent Filter で拾うよ。

外部のブラウザアプリなら普段ユーザーが使っているし、アプリ側から偽装はできないよね?
本物かどうか区別できるから安全だよね?ってコードがこちらです。
public class TwitterAuthActivity extends FragmentActivity {

    private AsyncTwitter mTwitter;
    private static final String CONSUMER_KEY = "Your CONSUMER_KEY";
    private static final String CONSUMER_SECRET = "your CONSUMER_SECRET";
    private static final String CALLBACK_URL = "yourapp://twitter_callback/";
    private RequestToken mRequestToken;

    private TwitterListener mTwitterListener = new TwitterAdapter(){
        @Override
        public void gotOAuthRequestToken(RequestToken token){
            mRequestToken = token;
            final Intent intent = IntentUtils.createOpenBrowserIntent(mRequestToken.getAuthorizationURL());
            startActivity(intent);
        }

        @Override
        public void gotOAuthAccessToken(AccessToken token){
            // save access token
            finish();
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        mTwitter = new AsyncTwitterFactory().getInstance();
        mTwitter.setOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
        mTwitter.addListener(mTwitterListener);
        mTwitter.getOAuthRequestTokenAsync(CALLBACK_URL);
    }

    @Override
    protected void onNewIntent(Intent intent){
        super.onNewIntent(intent);
        Uri uri = intent.getData();
        if(uri != null && uri.toString().startsWith(CALLBACK_URL)){
            String verifier = uri.getQueryParameter("oauth_verifier");
            if(verifier != null){
                LogUtils.d(verifier);
                mTwitter.getOAuthAccessTokenAsync(mRequestToken, verifier);
            }else{
                // User canceled!
                finish();
            }
        }else{
            // other
            finish();
        }
    }

}
<activity
    android:name=".TwitterAuthActivity"
    android:launchMode="singleTask" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:host="twitter_callback"
            android:scheme="yourapp" />
    </intent-filter>
</activity>
OAuthの認証にWebViewを使うのはやめよう - Shogo's Blog
ポイントは以下の点です。
  • Twitter へのアプリケーション登録時に Callback URL にテキトーなURLを入れておく
  • 独自スキーマを定義して,受け取れるようにしておく
  • getOAuthRequestToken 呼び出し時に,Callback URL を明示的に渡す
  • アクティビティの多重起動を防止しておく
確認すべきはカスタム URL スキーマが被った(被せられた)場合に
AccessToken を奪われる可能性があるけど大丈夫?という点。
特定条件下におけるOAuth 2.0の認可応答を奪取されるリスクとその対策について - r-weblife
今回は、「ネイティブアプリケーションからOAuth 2.0を使うとき、特定の条件下において、正規のClientではない悪意のある第3者に認可応答を持って行かれて、その結果Access Tokenを取得できちゃうリスクがあるよね。どうしようか。」っていう話です。

条件っていうのは、

OAuth 2.0のClientはネイティブアプリケーションであり、Client Credential(client_id/client_secret)を安全に管理できない
Clientは外部のブラウザを立ち上げ、ユーザーは認可要求のURLにアクセスする。この時にいわゆるImplicit GrantやAuthorization Code Grantを使う
ユーザーがリソースアクセスを許可した後、認可応答はカスタムURIスキームでClientに渡る
というところです。

なんでこれで認可応答を持って行かれるかというと、カスタムURIスキームの扱いです。

認可要求を送ったClientに認可応答が戻ってくれればよいのですが、カスタムURIスキームを被せてきた(たまたま一緒だっただけかもしれない)別のアプリケーションに認可応答が渡る可能性があると。

で、このあたりのルールが”アプリを選択させる”、“先にインストールした方が優先”、“後にインストールした方が優先”、とかOSに依存するので悩ましい。
Android アプリである限り ConsumerKey/ConsumerSecret は漏れる。
たぶんリバースエンジニアリングとかで。詳しい手法は知らない。
同様にして AndroidManifest.xml の Intent Filter で受け取るカスタム URL スキーマもわかるんじゃないかな?
ということで カスタム URL スキームを Intent Filter で拾う手法は
狙われたらアウト、狙われなければセーフ的なギリギリのラインで危険で安全な手法。非推奨
上のコードで実装して何かあっても責任取れませんってやつです。

カスタム URL スキームが被る(被せられる)から問題なんだよね?
じゃあ動的に生成すれば被せられることはないんじゃない?

@Override
public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);

    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_VIEW);
    filter.addCategory(Intent.CATEGORY_BROWSABLE);
    filter.addCategory(Intent.CATEGORY_DEFAULT);
    filter.addDataScheme("http"); // http じゃないとブラウザからレシーブできない
    filter.addDataAuthority(RandomStringUtils.randomAlphanumeric(20), null);
    registerReceiver(mIntentReceiver, filter);
}

private BroadcastReceiver mIntentReceiver = new BroadcastReceiver(){
    @Override
    public void onReceive(Context context, Intent intent){
        Uri uri = intent.getData(); // ドメイン部分しか取れない
    }
};
コメントに書いてある通り無理だった…。
<activity
    android:name=".TwitterAuthActivity"
    android:launchMode="singleTask" >
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:host="twitter_callback"
            android:pathPattern=".*"
            android:scheme="yourapp" />
    </intent-filter>
</activity>
    private static final String CALLBACK_URL = "yourapp://twitter_callback/" + RandomStringUtils.randomAlphanumeric(20);
より複雑にしてみたらどうだろうと思ったけどシンプルな方でも拾えるので意味がなかった。

ということでカスタム URL スキームを Intent Filter で受け取る手法を安全にするのは難しそうです。

ではどうすればいい?

OAuthの認証にWebViewを使うのはやめよう - Shogo's Blog
PIN コードを利用

一つ目の方法はPC版クライアントでよく使われる方法. 認証後にPINコードと呼ばれる数字が表示されるので,それをアプリに入力します. twiccaなんかでも使われてますね. Twitter へのアプリケーション登録のときにコールバックURLを入力しないとこの認証方式になります.
ユーザーが自分でアプリに戻ってきて PIN を入力してくれれば
他のアプリに認証情報を取られるということがないという原始的な安全性の担保方法です。
原始的だからこそ付け込む隙がなくて安全なわけですね。

PIN コードさえ利用すれば安全?

いまさらブログ: Androidの脆弱性(まとめ版)
WebViewの addJavascriptInterface()の危険性(参考)が、Android 3.x/4.0/4.1 ではWebViewそのものに存在する。
このためWebViewをそのまま使っているアプリや、WebViewのラッパにすぎない標準ブラウザに同様の脆弱性がある。(Android版ChromeはWebViewを使っていないので問題ないです)

(中略)

Android 標準ブラウザや WebView クラスを利用しているアプリで、細工されたウェブページを閲覧した際に、ユーザの意図に反して Android OS の機能を起動されたり、任意のコードを実行されたりする可能性があります。
Android 4.2 未満の標準ブラウザや WebView では、
入力してアプリに保存した ID とパスワードを読み取られる可能性があります。
PIN コードを利用しようが、カスタム URL スキームを Intent Filter で受け取ろうが
Android 4.2 未満の標準ブラウザや WebView を利用した時点で安全とは言い切れなくなります。
しかも自分のアプリで気をつけていても他のアプリに問題があれば
自分のアプリの保存した ID とパスワードを読み取られる可能性があるようです。
参考
Androidアプリセキュリティ〜WebViewの注意点(1)〜 | Android | Developers AppKitBox
Androidアプリセキュリティ〜WebViewの注意点(2)〜 | Android | Developers AppKitBox

どんなに頑張っても危険があってツラい…

iOS みたいに端末に設定したアカウントを使えるとか何か良い方法ないんですかね?

参考にしたサイトまとめ

OAuthの認証にWebViewを使うのはやめよう - Shogo's Blog
TwitterのOAuthの問題まとめ
TwitterのOAuthの問題の補足とか
特定条件下におけるOAuth 2.0の認可応答を奪取されるリスクとその対策について - r-weblife
いまさらブログ: Androidの脆弱性(まとめ版)
Androidアプリセキュリティ〜WebViewの注意点(1)〜 | Android | Developers AppKitBox
Androidアプリセキュリティ〜WebViewの注意点(2)〜 | Android | Developers AppKitBox