X OAuth2.0 が Twitter にまだ囚われている話
Twitter -> X
今年(2024)、5月17日ぐらいをさかいに、旧Twitterのドメインである twitter.com
から新しい x.com
へリダイレクトを開始したのが記憶に新しいかと思います。
それに伴い、裏側のAPIも順次 api.twitter.com
から api.x.com
へ移行が始まりました。
OAuth1.0aとOAuth2.0
私たち 株式会社luco が運営・開発している スコラボ (https://sucolab.jp) では、Twitter(X)新規登録/ログインにOAuth1.0aではなく比較的新しいOAuth2.0を採用しています。
スコラボではTwitter(X)のほかに、Googleでの新規登録/ログインも対応しております。これらの新規登録/ログインの実装は、バックエンドにLaravelを採用しているため laravel/socialite
というOAuthラッパーを使用して実装しています。
なぜTwitter(X)での新規登録/ログインで現在主流のOAuth1.0aではなくOAuth2.0を使用したか
1. Googleなど他の多くのサービスでは、現在OAuth2.0が主流(というかこれのみ)となっている
主な違いとして、OAuth1.0aや1.1ではステートフルだったのに対し、2.0ではステートレスとなっております。
そのため、認可の最初のリダイレクト時からコールバックまで処理の実装方法が大きく異なり、スコラボでもTwitter(X)での新規登録/ログインでのみ処理を分岐する必要があったため、ソースコード上でノイズとなっていました。
2. なぜかOAuth1.0aが不安定な時期があり、その期間OAuth2.0は元気だった
x.comが移行タイミングだったからなのかは不明ですが、Sentryにエラーが度々飛んできていました。
今後の運用を考えたときに、メールアドレスが取得できないなどの問題はあるものの Twitter(X) を用いた新規登録/ログインを安定して提供するために OAuth2.0 に移行することにしました。
問題の発見
基本的にOAuthやプロフィール情報の取得などのAPIは後方互換性があるため、問題はありません。
そのため、特に対応をせずとも api.twitter.com
が非推奨にならない限りはコードベースを変更する必要はないなと思っておりましたが、OAuthでの新規登録/ログイン時に問題を見つけました。
x.com
でメインで新規登録/ログインしているアカウントではないアカウントで認可処理が行われてしまうことがあるのです。
twitter.com
と x.com
とで新規登録/ログインしているアカウントが異なる"場合がある"のです。
現在、これらは Cookie のドメイン空間により分けられておりセッションをそれぞれ別で持つことができる状態になっていました。
通常、ユーザーは twitter.com
から x.com
へリダイレクトされたときにログアウトされた状態になっているため再度同じアカウントで新規登録/ログインします。しかし、人によっては複数アカウントがある場合もあり、最後に twitter.com
で使用したアカウントがメインではない可能性があります。その後、 x.com
でメインアカウントで新規登録/ログインすると先程述べたとおり、 twitter.com
と x.com
とで別々のアカウントにメインで新規登録/ログインした状態となってしまいます。
そしてここからが重要で、現在OAuth1.0aは twitter.com
を x.com
へリダイレクトしますが、OAuth2.0ではリダイレクトされません。
フォーラムでのユーザーによる報告
この状態で、スコラボ (https://app.sucolab.jp) にTwitter(X)で新規登録/ログインしようとすると、 x.com
ではなく twitter.com
で認可処理が始まってしまい、ユーザーが意図していないアカウントで新規登録/ログインされてしまいます。
また、この際のOAuth2.0の画面上ではアカウントをスイッチすることができず、また twitter.com
上でアカウントを変えようにもリダイレクトされてしまうため一般ユーザーでは twitter.com
のセッションを書き換えることもできなくなってしまいました。
解決法
私たちはこの問題を見つけた当初、OAuth1.0aへ戻すことも検討しましたが、 laravel/socialte
内のコードをいじって認可に使うURLを直接 api.x.com
へ買い替えたらどうなる?と検証してみました。
すると、 x.com
上でOAuth2.0が動きそのままコールバックまで正しく動作することがわかりました。
そのため、コントリビューションチャンスということで laravel/socialite
へプルリクエストを出しつつ、私たち側のバックエンドでは一旦 laravel/socialite
にカスタムプロバイダを追加してURLを上書きすることにしました。
実際のPR
upstreamにマージされるまでのバックエンドでの暫定対応
AppServiceProvider
の register
メソッド内で SocialiteManager
をDIに登録します。
SocialiteManager
は Illuminate\Support\Manager
を拡張しているため、 extend
を用いて 'twitter'
という名前のドライバをカスタムした XProvider
で上書きします。
この際、トークン等のクレデンシャルも一緒に渡しておきます(OAuth2.0が有効なトークンである必要があります)。
<?php
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// ...
$this->app->singleton(SocialiteManager::class, static function ($app) {
$manager = new SocialiteManager($app);
return $manager->extend('twitter', static function ($app) use ($manager): AbstractProvider {
return $manager->buildProvider(XProvider::class, $app['config']['services.twitter']);
});
});
}
次に、上記で使用した XProvider
を用意します。
内容は上記で作成したPRでの実装内容と同じです。OAuth2.0である Laravel\Socialite\Two\TwitterProvider
を extends し、内部で twitter.com
と使っている箇所すべてを x.com
で置き換えているだけのかんたんな実装です。
場所はお好きな場所においてください。
<?php
declare(strict_types=1);
namespace App\Utils\Socialite;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Arr;
use Laravel\Socialite\Two\TwitterProvider;
class XProvider extends TwitterProvider
{
public function getAuthUrl($state): string
{
! return $this->buildAuthUrlFromBase('https://x.com/i/oauth2/authorize', $state);
}
protected function getTokenUrl(): string
{
! return 'https://api.x.com/2/oauth2/token';
}
protected function getUserByToken($token)
{
! $response = $this->getHttpClient()->get('https://api.x.com/2/users/me', [
RequestOptions::HEADERS => ['Authorization' => 'Bearer ' . $token],
RequestOptions::QUERY => ['user.fields' => 'profile_image_url'],
]);
return Arr::get(json_decode((string)$response->getBody(), true), 'data');
}
}
そして最後に、 composer.json
で Laravel Package Auto Discovery
を無効化しておきます。
laravel/socialite
は、 Laravel Package Auto Discovery
機能を用いて自動的にboot時にDIを行いますが、今回は上書きをする都合上手動で登録しているので無効にします。
{
...
"extra": {
"laravel": {
"dont-discover": [
+ "laravel/socialite"
]
}
}
}
これらの実装を行うことで、Twitter(X)での新規登録/ログイン時にうまく x.com
へ遷移し、 x.com
でメインで使用しているアカウントを用いて新規登録/ログインすることができるようになりました。意図していないアカウントの場合でも、 x.com
上であればユーザー側がアカウントスイッチするだけで解決できます。
最後に
TwitterからXへ移行した際に起きたOAuth2.0での問題とその解決法についてご紹介いたしました。
対応中に作成した upstream へのプルリクエストも後日マージ、そしてリリースしていただきました。
(Laravelあるあるだが無言マージ怖い)OSSのコントリビュートもしつつ、 laravel/socialite
の抽象化のおかげでupstreamでレビュー・マージされるまでの暫定対応もできたので柔軟に対応できたかと思います。
宣伝
株式会社luco では現在一緒に配信を盛り上げ、未来を創る仲間を探しております!
業務中のOSS活動はもちろん、使用する技術の選定なども柔軟に、かつ面白い方へと進めていける環境が整っています。新しい技術に挑戦したい方や、自由な発想でプロジェクトに取り組みたい方にとって、最適な場所です。
また、私たち 株式会社luco はPHP Conference 2024 に協賛しています🎉
気になった方は何卒よろしくお願いいたします。
Discussion