🐦

【Laravel】TwitterOAuth2.0でログインしてツイートする

2022/06/09に公開

この記事について

このスクラップでやったことを清書したものです。
https://zenn.dev/himawarichan27/scraps/37e806dc8b62ff

TwitterにログインしツイートするプログラムをLaravelで実装します。
APIはTwitterV2を使い、認証はOAuth2.0を仕様します。

認証の処理はLaravel Socialiteを、ツイート処理はTwitterOAuthを使用しますが、TwitterOAuthは2022年6月現在OAuth2.0に正式に対応していないため無理やりな使い方をしています。なのでTwitterOAuthを使わずに普通にHttpクライアントを使って処理を書いた方がいいかもしれません。

なおTwitterのDeveloperアカウントは作成済みとします。
またOAuth周りの説明もしないので、よくわかっていない場合は下記の記事を先に読んでおくとわかりやすいかもしれません。
https://qiita.com/nobuo_hirai/items/8cd8140e7d3970e4e094

環境

Laravel 8.83.14 ※laradockで構築済み
mysql 8.0.28
Laravel Socialite 5.5.2
TwitterOAuth 4.0.0

事前準備

1.ライブラリのインストール

TwitterOAuth、LaravelSocialite、SocialiteのTwitter用アダプタをcomposerでインストール

composer require abraham/twitteroauth
composer require laravel/socialite
composer require socialiteproviders/twitter

ファサードを設定し、アダプタをイベント登録します。

config/app.php
'providers' => [
  //~~略~~
    \SocialiteProviders\Manager\ServiceProvider::class, 
],
app/Providers/EventServiceProvider.php
    protected $listen = [
        SocialiteWasCalled::class => [
            'SocialiteProviders\\Twitter\\TwitterExtendSocialite@handle',
        ],
    ];

2.TwitterDeveloperで各種キーを発行

Twitter developerでクライアントキーとクライアントシークレットを発行します。
作成したアプリの設定画面から「User authentication settings」のEditをクリック。

こんな画面が出てくるので下記のように変更します。

  • OAuth2.0をON
  • 「Type of Web」で該当するものを選択。今回は「WebApp」でOK。
  • 「Callback URI / Redirect URL」に認証画面からリダイレクト後戻ってくるURLを入力。ローカル環境で実行するだけならlocalhostでもOK。
  • 「Website URL」にAPIを使うサイトのURLを入力。localhostやhttp://127.0.0.1/ にすると怒られたが、APIのリクエスト時にチェックが入るようではなさそうなので自分のTwitterアカウントのURLなど、わりと適当で大丈夫そう。

「Save」をクリックするとクライアントキーとクライアントとシークレットが発行されます。
再確認はできないので必ず控えてください。 控えるのを忘れた場合は再発行してください。

実装

1. ページ構成とルーティング

いよいよ実装を進めていきます。
その前に今回の処理フローとページ構成はこんな感じ。

このフローをルーティングにするとこう。

routes/route.php
Route::prefix('twitter')->group(function () {
    //ログイン
    Route::match(['get', 'post'], 'login', [App\Http\Controllers\Twitter\MyPageController::class, 'login'])->name('twitter.mypage.login');
    //認証リダイレクト
    Route::get('callback', [App\Http\Controllers\Twitter\MyPageController::class, 'callback'])->name('twitter.mypage.callback');
    //ツイートページ
    Route::group(['middleware' => 'twitter'], function () {
        Route::match(['get', 'post'], 'mypage', [App\Http\Controllers\Twitter\MyPageController::class, 'index'])->name('twitter.mypage.index');
    });
});

ツイートページにはログインしていないアカウント以外アクセスしてほしくないので、ミドルウェアでログイン判定を行っています。本当はLaravelのちゃんとした認証機能を使った方がいいのでしょうが、そこはメインではないので今回はこれで…

app/Http/Middleware/LoginTwitter.php
class LoginTwitter {
    /**
     * Twitterログイン保持チェック
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */
    public function handle(Request $request, Closure $next) {
        if (!$request->session()->get('twitter_id')) {
            return redirect()->route('twitter.mypage.login');
        }
        return $next($request);
    }
}

2. テーブル作成

ユーザーのアクセストークンを保存するためのテーブルを用意します。
今回DBに格納したいデータは、

  • TwitterのID(@からはじまるやつ)
  • アクセストークン
  • アクセストークンの有効期限

OAuth1.0ではアクセストークンに有効期限はありませんでしたが、2.0では二時間の有効期限があります。なのでアクセストークンの有効期限を保存しておく必要があります。

次のようなマイグレーションを作ります。

/**
* Run the migrations.
*
* @return void
*/
public function up()
{
    Schema::create('tb_twitter_users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('twitter_id');
    $table->string('access_token');
    $table->dateTime('token_limit');
    $table->timestamps();
    });
}

マイグレーションを実行し出来上がったテーブルはこちら。

mysql> desc tb_twitter_users;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | int unsigned | NO   | PRI | NULL    | auto_increment |
| twitter_id    | varchar(255) | NO   |     | NULL    |                |
| access_token  | varchar(255) | NO   |     | NULL    |                |
| token_limit   | datetime     | NO   |     | NULL    |                |
| created_at    | timestamp    | YES  |     | NULL    |                |
| updated_at    | timestamp    | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+

3. 各種キーのセット

TwitterDeveloperで発行したクライアントキーなどをLaravel側に設定します。

.env
TWITTER_CLIENT_ID=<自分のクライアントキー>
TWITTER_CLIENT_SECRET=<自分のクライアントシークレット>
TWITTER_CLIENT_CALLBACK=<認証後のコールバックURL>
config/services.php
'twitter' => [
    'client_id'       => env('TWITTER_CLIENT_ID'),
    'client_secret'   => env('TWITTER_CLIENT_SECRET'),
    'redirect'        => env('TWITTER_CLIENT_CALLBACK'),
    'oauth'           => 2,
]

OAuth2.0で認証する場合は「'oauth' => 2」と書く必要があります。
1.0で認証するなら書く必要はありません。

4.ログイン処理

ログインボタンをクリックしTwitterの認証ページまでリダイレクトするところまでの処理を作っていきます。
viewは割愛しますが、「Twitterにログインする」というボタンが置いてあるだけです。

app/Http/Controllers/Twitter/MyPageController.php
namespace App\Http\Controllers\Twitter;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Laravel\Socialite\Facades\Socialite;
use App\Models\TbTwitterUsers;

class MyPageController extends Controller
{
    public function login(Request $request)
    {
        if (!$request->isMethod('post')) {
            return view('twitter.mypage.login');
        }
        //ログインボタンを押したらTwitterの認証ページへ
        return Socialite::driver('twitter')
            ->scopes(['tweet.read', 'tweet.write', 'users.read'])
            ->redirect();
    }

code_verifierとかcode_challengeとか色々やらなきゃいけないところがこれだけでOK。すごいなSocialite。
今回はツイートしたいのでscopeに「tweet.read」「tweet.write」を指定しています。writeだけじゃなくてreadも必要なので注意。
やりたいことに応じてどういうscopeがいるのかは下記を見てください。
https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code

5.コールバック処理

続いてTwitterの認証画面からリダイレクトしてきた後のコールバック処理を作ります。

app/Http/Controllers/Twitter/MyPageController.php
public function callback(Request $request)
    {
        try {
            //ユーザー情報取得
            $user = Socialite::driver('twitter')->user();
        } catch (\Exception $e) {
            return redirect('twitter.mypage.login')->with('oauth_error', '予期せぬエラーが発生しました');
        }

        //トークンが期限切れになる時間
        $expires_in = $user->expiresIn;
        $expire_time = new Carbon('+' . $expires_in . ' seconds');

        $upsert_array = [
            'twitter_id' => $user->getNickname(),
            'access_token' => $user->token,
            'token_limit' => $expire_time,
        ];
        //TwitterIDを条件にupsert
        $result = TbTwitterUsers::updateOrCreate(['twitter_id' => $upsert_array['twitter_id'] ?? NULL], $upsert_array);

        //セッションにTwitterIDを保存
        $request->session()->put('twitter_id', $result->twitter_id);

        return redirect(route('twitter.mypage.index'))->with('flash_message', 'ログインしました。');
    }

無事に認証されれば$user = Socialite::driver('twitter')->user();だけでユーザーの情報を読み取れるようになります。もうこの時点でアクセストークンも発行されています。本来であればAuthorization Codeとアクセストークンを交換する処理が必要なのに。便利すぎ。

必要情報をDBに保存し、セッションにTwitterIDを渡して、ツイートページにリダイレクトする。

6.ツイート処理

最後にログインしたアカウントからツイートする処理を作っていきます。

まずはDBからアクセストークンを取得し、もしアクセストークンの期限が切れていた場合はログインページにリダイレクトさせます。

app/Http/Controllers/Twitter/MyPageController.php
public function index(Request $request)
{
    if (!$request->isMethod('post')) {
        return view('twitter.mypage.index');
     }

    $twitter_id = $request->session()->get('twitter_id');
    //アクセストークンをDBから取得
    $twitter_user = TbTwitterUsers::getFirstRecordByTwitterId($twitter_id);

    $access_token = $twitter_user->access_token;
    $client_id = config('services.twitter.client_id');
    $client_secret = config('services.twitter.client_secret');

    //アクセストークンの期限が切れていたらログインページへリダイレクト
    if ($twitter_user->token_limit->lt(Carbon::now())) {
        return redirect(route('twitter.mypage.login'))->with('oauth_error', 'もう一度ログインしてください。');
    }

トークンが有効期限内であればTwitterOAuthを使ってツイートするAPIにリクエストを投げます。

app/Http/Controllers/Twitter/MyPageController.php
    //ツイートする
    $twitter = new TwitterOAuth(
        $client_id,
        $client_secret,
        null,
        $access_token,
    );
    $twitter->setApiVersion("2");

    $twitter->post("tweets", ["text" => $request->input('tweet')], true);
    if ($twitter->getLastHttpCode() == 201) {
        $result_message = 'ツイートしました。';
    } else {
        $result_message = 'ツイートに失敗しました。';
    }

    return view('twitter.mypage.index', ['flash_message' => $result_message]);

冒頭にも書きましたが、TwitterOAuthは2022年6月時点ではOAuth2.0に対応していないようです。
https://github.com/abraham/twitteroauth/issues/1041

ドキュメントによると、TwitterOAuthのインスタンスを作る際は第三引数にアクセストークンを、第四引数にアクセストークンシークレットを渡すとありますが、OAuth2.0にアクセストークンシークレットは存在しません。

ただし第三引数をnullにし第四引数にアクセストークンを渡せばそれを使ってくれるようだったので、このように書いています。
とりあえずこれで動いていますがあっているのかわからないので、Httpクライアント使ってリクエストした方がいいかもしれませんね…。

これでログインしてつぶやくことができるようになりました!

リフレッシュトークンについて

前述しましたがアクセストークンは2時間で有効期限が切れてしまいます。
しかし12時間後にツイートするような予約投稿機能をつけたい場合、それでは困ってしまいますよね?

そういうときに使うのが「リフレッシュトークン」です。
リフレッシュトークンはscopeに「offline.access」を指定した場合にアクセストークンと一緒に発行されます。リフレッシュトークンを使うと、ユーザーの認証なしに新しいアクセストークンを再発行できます。

先ほどの処理にリフレッシュトークンを保存し交換する処理を付け加えてみましょう。

テーブルにカラムを追加

refresh_tokenカラムを増やしました。

mysql> desc tb_twitter_users;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | int unsigned | NO   | PRI | NULL    | auto_increment |
| twitter_id    | varchar(255) | NO   |     | NULL    |                |
| access_token  | varchar(255) | NO   |     | NULL    |                |
| token_limit   | datetime     | NO   |     | NULL    |                |
| refresh_token | varchar(255) | NO   |     | NULL    |                |
| created_at    | timestamp    | YES  |     | NULL    |                |
| updated_at    | timestamp    | YES  |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+

ログイン処理を変更

scopeに「offline.access」を追加するだけです。

app/Http/Controllers/Twitter/MyPageController.php
 return Socialite::driver('twitter')
    ->scopes(['tweet.read', 'tweet.write', 'users.read', 'offline.access'])  //'offline.access'を追加
    ->redirect();

コールバック処理を変更

リフレッシュトークンもDBに保存するようにするだけです。

app/Http/Controllers/Twitter/MyPageController.php
$upsert_array = [
    'twitter_id' => $user->getNickname(),
    'access_token' => $user->token,
    'token_limit' => $expire_time,
    'refresh_token' => $user->refreshToken,  //追加
];
//TwitterIDを条件にupsert
$result = TbTwitterUsers::updateOrCreate(['twitter_id' => $upsert_array['twitter_id'] ?? NULL], $upsert_array);

ツイート処理を変更

アクセストークンの有効期限が切れていたらリフレッシュトークンでアクセストークンを再発行する処理に変更します。

app/Http/Controllers/Twitter/MyPageController.php
//アクセストークンの期限が切れていたらリフレッシュトークンで取得し直す
if ($twitter_user->token_limit->lt(Carbon::now())) {
    $url = 'https://api.twitter.com/2/oauth2/token';

    $response = Http::withHeaders([
        'Accept' => 'application/json',
    ])->withBasicAuth($client_id, $client_secret)->post($url, [
        'grant_type' => 'refresh_token',
        'client_id' => $client_id,
        'client_secret' => $client_secret,
        'refresh_token' => $twitter_user->refresh_token,
        'client_id' => $client_id,
    ]);
    $response_array = json_decode($response->getBody(), true);

    //新しいトークンをDBに保存
    $access_token = $response_array['access_token'];
    $expires_in = $response_array['expires_in'];
    $expire_time = new Carbon('+' . $expires_in . ' seconds');

    $upsert_array = [
        'twitter_id' => $twitter_id,
        'access_token' => $access_token,
        'token_limit' => $expire_time,
        'refresh_token' => $response_array['refresh_token'],
    ];
    TbTwitterUsers::upsert($upsert_array);
}

残念ながらSocialiteではリフレッシュトークンとアクセストークンの交換はできないようです。なので普通にHttpクライアントでリクエストします。
新しいアクセストークンを受け取ったらDBに保存します。またリフレッシュトークンも新しいものが発行されているので合わせて保存しておきましょう。

これでアクセストークンの有効期限が切れても、ユーザーに再度認証してもらわなくても呟けるようになりました。

おわりに

TwitterがOAuth2.0に対応したのはわりと最近のようで、ウカツにも「せっかくだから新しい仕組みを使お~」などと考えてしまったためにめちゃくちゃ苦しんだので書き残すことにしました。
わかっちゃえばすごい簡単だし、何よりLaravel Socialiteすげー!!

スクラップでやったことからアドリブで色々変えちゃったので、変なところあったらすみません。
PHPerの皆様のTwitterOAuth2.0対応の助けとなれば幸いです。

参考

Laravel Socialiteドキュメント
https://readouble.com/laravel/8.x/ja/socialite.html
Laravel Socialiteの使い方(4系)
https://www.ritolab.com/entry/207
Laravel Socialiteのコード解説
https://qiita.com/near108/items/820058f690b4f0f4192a
リフレッシュトークンについて
https://thinca.hatenablog.com/entry/creating-twitter-bot-memo

Discussion