OAuth認証のフローをAndTweetのソースコードで理解する

OAuth認証について勉強するために、オープンソースTwitterクライアントAndTweetソースコードを読んでみます。

OAuth認証の仕組みについては、id:yuroyoroさんの記事、OAuthプロトコルの中身をざっくり解説してみるよがメチャメチャわかりやすいです。

以下、上記の記事で解説されている「認証のざっくりした手順」に、AndTweetのコードを照らし合わせることで、OAuth認証への理解を深めます。(1,2,3...などの手順の文言は記事からの引用です。)

なお、AndTweetのダウンロード、デバッグ環境構築の仕方はオープンソースのTwitterクライアントAndTweetのソースコードをダウンロードしてビルドするを参照してください。

1. ユーザは、Consumer(AndTweet)にOAuth認証を行うように指示します。

記事には、「通常は、Consumerのページにあるログインボタンなどをクリックします。」とありますが、Andtweetの場合は、以下のようになります。

(1) Preferenceがタップされたタイミングで、OAuth認証をスタート

// PreferencesActivity.java
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
    ...
    if (preference.getKey().compareTo(KEY_VERIFY_CREDENTIALS) == 0) {
        verifyCredentials(true);
    }
    ...
};

(2) まだ認証されていなければ、OAuthAcquireRequestTokenTaskを実行

// PreferencesActivity.java
private void verifyCredentials(boolean reVerify) {
    ...
    if (reVerify || mUser.getCredentialsVerified() == CredentialsVerified.NEVER) {
        ...
        new OAuthAcquireRequestTokenTask().execute();
    }
}

次は、OAuthAcquireRequestTokenTaskの内容です。

2. Consumer(Andtweet)は、Provider(Twitter)からリクエストークンを取得します

(1) ConsumerからProviderへHttp通信でリクエストークンを要求します。
(2) リクエストークンの要求で、事前に発行されているConsumer Keyと、リクエストパラメータをConsumer Secretで署名した値をパラメータとして付与します。
(3) ProviderはHttpのレスポンスとしてリクエストークンを返します。この段階では、まだ認証は完了していません。

// PreferencesActivity.java
private class OAuthAcquireRequestTokenTask extends AsyncTask<Void, Void, JSONObject> {
    ...
    @Override
    protected JSONObject doInBackground(Void... arg0) {
        ... 
        // Consumer KeyとConsumer SecretでConsumerインスタンスを作成する。
        // CommonsHttpOAuthConsumerクラスは、oauth-signpostというライブラリに含まれている。
        mConsumer = new CommonsHttpOAuthConsumer(OAuthKeys.TWITTER_CONSUMER_KEY,
                    OAuthKeys.TWITTER_CONSUMER_SECRET);
                    
        // Twitterで公開されているOAuth認証用のURLでProviderのインスタンスを作成する。
        // CommonsHttpOAuthProviderクラスも、oauth-signpostというライブラリに含まれている。
        mProvider = new CommonsHttpOAuthProvider(ConnectionOAuth.TWITTER_REQUEST_TOKEN_URL,           // "https://api.twitter.com/oauth/request_token"
                    ConnectionOAuth.TWITTER_ACCESS_TOKEN_URL, ConnectionOAuth.TWITTER_AUTHORIZE_URL); // "https://.../access_token", "https://.../authorize"
        ...
        
        // ProviderにConsumerのインスタンスとコールバックURLを渡してリクエストトークンを要求する。 (1), (2)
        // 同時に、発行されたリクエストトークンが付加された認証用URLを返す。
        String authUrl = mProvider.retrieveRequestToken(mConsumer, CALLBACK_URI.toString());
        
        // Providerはレスポンスとしてリクエストトークンを返して、Consumerにセットする。(3)
        // SharedPereferenceに保存しておく。
        saveRequestInformation(tu.getSharedPreferences(), mConsumer.getToken(), mConsumer.getTokenSecret());
        ...
    }
    ...
}

3. 認証用URLへのリダイレクト・ユーザ承認

(1) Consumerは、発行されたリクエストトークンをURLに付与して、Providerの認証用URLへリダイレクトを行います。

// PreferencesActivity.java
private class OAuthAcquireRequestTokenTask extends AsyncTask<Void, Void, JSONObject> {
    ...
    @Override
    protected JSONObject doInBackground(Void... arg0) {
        ...
        // 3の続き 
        // 発行されたリクエストトークンが付加された認証用URLをブラウザで開く(1)
        PreferencesActivity.this.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl)));
        ...
    }
    ...
}

(2) リダイレクト先で、Providerがユーザに対して、Consumerが要求しているOAuth認証によるAPI利用を許可するか選択します。
ブラウザで、以下のような「[アプリ名]があなたのアカウントを利用することを許可しますか?」のURLが開かれます。


ちなみに、このURLではユーザー名やパスワードがブラウザのクッキーに保存されるので、1度認証した後は、「ユーザー名やメールアドレス:」、および「パスワード:」の入力欄は表示されません。

(3) 承認した場合は、Providerは(通常は)Consumer登録時に設定したコールバックURLへリダイクレトします。
[アプリを認証]ボタンが押されると、Twitter(ブラウザ)は、Consumer登録時に設定したコールバックURLへリダイレクトします。リダイレクトはURL付きのIntentとして発行されるので、これをアプリ側で受け取ります。

// PreferencesActivity.java
@Override
protected void onResume() {
   ...
    Uri uri = getIntent().getData();
    ...
    if (uri != null) {
        ...
        if (CALLBACK_URI.getScheme().equals(uri.getScheme())) {
            ...         
            // This activity was started by Twitter ("Service Provider")
            // so start second step of OAuth Authentication process
            new OAuthAcquireAccessTokenTask().execute(uri);
        }
    }
}
// AndroidManifest.xml
<activity android:name=".PreferencesActivity"
    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:scheme="andtweet-oauth" android:host="twitt" />
    </intent-filter>
</activity>

アプリ側でURL付きのインテントを受け取ったら、次は、OAuthAcquireAccessTokenTaskでアクセストークンを取得します。

【参考】AndroidManifest.xmlにIntentFilterを記述し忘れた場合などで受け手が見つからなければ、僕の環境ではIntentをブラウザが受け取りました。しかし、ブラウザにはこのURLは表示できないとのエラーメッセージが出力されます。このとき、以下のようなエラーログが出力されました。

05-21 21:28:13.255: ERROR/browser(7042): onReceivedError -10 andtweet-oauth://twitt?oauth_token=EO9hB6PN5wUm9rknrfLXXXXXXXXXXXXXXXXXXXX&oauth_verifier=SI2WkipVnVXUspyNZd9ikAXXXXXXXXXXXXXXXXXXXX このプロトコルには対応していません。

4. アクセストークンの取得

(1) コールバックURLへのリダイレクトで、Consumerはリクエストークンをもとにアクセストークン取得をHttp通信でProviderへ要求します。
(2) アクセストークン取得要求は、Consumer Keyとリクエストークンなどをパラメータに付与して呼び出します。(通常はAuthorizationヘッダに設定)
(3) アクセストークン取得要求のパラメータも、Consumer Secretで署名した値を付与します。
(4) Providerは、レスポンスとしてアクセストークンを返します。

// PreferencesActivity.java
private class OAuthAcquireAccessTokenTask extends AsyncTask<Void, Void, JSONObject> {
    ...
    @Override
    protected JSONObject doInBackground(Uri... uris) {
        ...
        TwitterUser tu = TwitterUser.getTwitterUser(PreferencesActivity.this);
        
        Uri uri = uris[0];
        if (uri != null && CALLBACK_URI.getScheme().equals(uri.getScheme())) {
            // リクエストトークンとシークレット
            String token = tu.getSharedPreferences().getString(ConnectionOAuth.REQUEST_TOKEN, null);
            String secret = tu.getSharedPreferences().getString(ConnectionOAuth.REQUEST_SECRET, null);
            ...
            ConnectionOAuth conn = ((ConnectionOAuth) tu.getConnection());
            ...
            mConsumer.setTokenWithSecret(token, secret);  // (2), (3)
            ...
            // アクセストークン取得をProviderへ要求する (1)
            mProvider.retrieveAccessToken(mConsumer, verifier);
            
            // Providerはアクセストークンとシークレットを返す (4)
            token = mConsumer.getToken();
            secret = mConsumer.getTokenSecret();
            ...
            conn.saveAuthInformation(token, secret); // SharedPreferencesに保存しておく
            ...
        }
    }
    ...
}

アクセストークン取得の流れはこんな感じです。もう少しコードの中身を見てみたいですが、今回はフローを理解することが目的なので省略します。

5. OAuthでのAPI呼び出し

(1) 実際のAPI呼び出しは、取得したアクセストークンをパラメータに付与して呼び出します。
(2) アクセストークンとConsumer Key、およびパラメータをConsumer Secretで署名した値を、Authorizationヘッダに設定に設定してAPIを呼び出すことで、OAuth認証を利用したAPI呼び出しができます。

// ConnectionOAuth.java
private JSONObject postRequest(HttpPost post) throws ConnectionException,
        ConnectionAuthenticationException, ConnectionUnavailableException,
        SocketTimeoutException {
    ...
    // リクエストヘッダに、取得したアクセストークンを付ける
    mConsumer.sign(post);
    String response = mClient.execute(post, new BasicResponseHandler());
    ...
}
// oauth.signpost.AbstractOAuthConsumer.AbstractOAuthConsumer
public HttpRequest sign(HttpRequest request) throws OAuthMessageSignerException,
            OAuthExpectationFailedException, OAuthCommunicationException {
    ...
    requestParameters = new HttpParameters();
    ...
    collectHeaderParameters(request, requestParameters);
    collectQueryParameters(request, requestParameters);
    collectBodyParameters(request, requestParameters);
    
    // リクエストヘッダに、oauth_consumer_key, oauth_nonce, oauth_signature_method, oauth_timestamp, 
    // oauth_token, oauth_versionを付ける
    completeOAuthParameters(requestParameters);
    ...
}

以上です。