Laravel9系,Nuxt.js2系でSNSログイン(Twitter,FaceBook,LINE)機能を実装する

Nuxt.js2系(compositon API)とLaravel9系+Laravel Socialite + Sanctum + Fortifyを使ってTwitter,facebook,LINEのログイン機能を実装します。

少し今回と開発環境は違いますがSanctumとFortifyのinstall方法については別記事にまとめてますのでそちらをご確認ください。
https://saunabouya.com/2022/01/27/laravel8-fortify-sanctum-vue-js3-compositon-api-1/

今回、SNSログインを実装するにあたり下記の記事を参考にしました。
https://qiita.com/hareku/items/ea09602bf40bf0a42040

実装方法

まず事前に以下のサイトからアプリケーションを作成する必要があります。
アプリケーションの作成方法は今回は割愛します。。。

https://developer.twitter.com/ja
https://developers.facebook.com/
https://account.line.biz/login?redirectUri=https%3A%2F%2Fdevelopers.line.biz%2Fconsole%2F%3Fstatus%3Dcancelled%26status%3Dcancelled

Laravel側

Laravel Socialiteをインストール

composer require laravel/socialite
composer require socialiteproviders/twitter
composer require socialiteproviders/line

EventServiceProvider.php

protected $listen = [
    // 省略
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        'SocialiteProviders\\Twitter\\TwitterExtendSocialite@handle',
    ],
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        'SocialiteProviders\\Line\\LineExtendSocialite@handle',
    ],
];

configファイル

'providers' => [
  //
    Laravel\Socialite\SocialiteServiceProvider::class, 
    \SocialiteProviders\Manager\ServiceProvider::class,
],

'aliases' => Facade::defaultAliases()->merge([
        'Socialite' => Laravel\Socialite\Facades\Socialite::class,
    ])->toArray(),
"twitter" => [
    "client_id" => env("TWITTER_AUTH_CLIENT_ID"),
    "client_secret" => env("TWITTER_AUTH_CLIENT_SECRET"),
    "redirect" => env("TWITTER_CALLBACK_URL"),
],
'facebook' => [
    'client_id' => env('FACEBOOK_APP_ID'),
    'client_secret' => env('FACEBOOK_APP_SECRET'),
    'redirect' => env('FACEBOOK_CALLBACK_URL'),
],
'line' => [
    'client_id' => env('LINE_CLIENT_ID'),
    'client_secret' => env('LINE_CLIENT_SECRET'),
    'redirect' => env('LINE_REDIRECT_URI')
],

.env

注意点としましてはFacebookログインを実装するにはhttpsが必須です。
そのため、docker環境下で開発を進める際などはローカル環境でhttpsを有効にしなければなりません

私はLaravel Sailで開発を進めていたので以下のリンクを参考にしました。
https://lotus-base.com/blog/37/

Nuxt側
https://qiita.com/nobita_x009/items/0ce13d7ba08a287fbf94

TWITTER_AUTH_CLIENT_ID= //Twitterから取得したAPI key
TWITTER_AUTH_CLIENT_SECRET= //Twitterから取得したAPI key secret
TWITTER_CALLBACK_URL=https://hogehoge/oauth/twitter/callback
TWITTER_ACCESS_TOKEN= //Twitterから取得したACCESS_TOKEN
TWITTER_ACCESS_TOKEN_SECRET= //Twitterから取得したACCESS_TOKEN_SECRET

FACEBOOK_APP_ID=  //facebookから取得したAPI key
FACEBOOK_APP_SECRET= //facebookから取得したAPI key secret
FACEBOOK_CALLBACK_URL=https://hogehoge/oauth/facebook/callback

LINE_CLIENT_ID= //取得したチャネルID
LINE_CLIENT_SECRET= //取得したチャネルシークレット
LINE_REDIRECT_URI=https://hogehoge/oauth/line/callback

ルーティング

TwitterやFacebook以外のサービスを使用したログインを実装することを想定して以下のように{provider}の部分にはサービス名が入るようにします。

Route::group([
    'namespace' => 'App\Http\Controllers\User',
], function () {
    Route::get('/oauth/{provider}', 'SocialiteController@redirectToProvider');
    Route::get('/oauth/{provider}/callback', 'SocialiteController@handleProviderCallback');
    Route::post('/oauth/{provider}/register', 'SocialiteController@store');
});

// ログイン認証後
Route::group(['middleware' => ['auth:sanctum']], function () {
    // ログインチェック
    Route::get('/user', function (Request $request) {
        return Auth::user();})->name('user');
});

Userテーブル編集

SNS認証に対応するためにpasswordはnullableに設定しました。
また、新たにどのSNSからログインされたのかを記録するproviderと
SNSのIDを記録するprovider_idを追加しています。

 Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('provider')->nullable();
            $table->string('provider_id')->unique()->nullable()->comment('ソーシャルメディアのID');
            $table->string('name')->nullable();
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });

モデル

今回追加したproviderとprovider_idを$fillable配列に追加して保存できるようにします。

protected $fillable = [
        'name',
        'email',
        'provider',
        'provider_id',
        'password',
    ];

コントローラー

コントローラにメソッドを追加します。

redirectToProviderではログインボタンを押された時に、SNS側ページにリダイレクトするためのエンドポイントを返します。

handleProviderCallbackではSNS側からリダイレクトを受けるエンドポイントを返します。
SNS側から受け取った値(provider_id)が既存のUserテーブルにあるか確認し、もしprovider_idがDBにあればログインさせ、なければ登録画面に戻します。

<?php

namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Exception;
use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\RegisterResponse;

class SocialiteController extends Controller
{
    /**
     * The guard implementation.
     *
     * @var \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected $guard;

    /**
     * Create a new controller instance.
     *
     * @param  \Illuminate\Contracts\Auth\StatefulGuard  $guard
     * @return void
     */
    public function __construct(StatefulGuard $guard)
    {
        $this->guard = $guard;
    }
    //認証ページへユーザーをリダイレクト
    public function redirectToProvider($provider)
    {
        return response()->json([
            'redirect_url' => Socialite::driver($provider)->redirect()->getTargetUrl()
        ]);
    }

    //ログイン
    public function handleProviderCallback($provider)
    {
        //認証結果の受け取り
        if ($provider !== 'twitter') {
            try {
                $user = Socialite::with($provider)->stateless()->user();
            } catch (Exception $e) {
                return redirect(config('app.front_url'). '/login'); //Nuxt.js側のURL
            }
        }
        if ($provider === 'twitter') {
            try {
                // なぜかSessionからTwitterの認証結果が取れないためアクセストークンで取得
                $user = Socialite::driver($provider)
                    ->userFromTokenAndSecret(env('TWITTER_ACCESS_TOKEN'), env('TWITTER_ACCESS_TOKEN_SECRET'));
            } catch (Exception $e) {
                return redirect(config('app.front_url') . '/login'); //Nuxt.js側のURL
            }
        }
        $authUser = User::where('provider_id', $user->id)->first();
        //provider_idがDBにあればログインさせる
        if ($authUser) {
            Auth::guard()->login($authUser, true);
            return app(LoginResponse::class);
        }

        return response()->json($user, 200);
    }

    public function store(Request $request, $provider)
    {
        User::create([
            'provider'      => $provider,
            'provider_id'   => $request->provider_id,
            'name'          => $request->name,
            'email'         => $request->email,
        ]);

        $user = User::where('provider_id', $request->provider_id)->first();
        event(new Registered($user));
        $this->guard->login($user);
        return app(RegisterResponse::class);
    }

}

Twitterのユーザー情報を取得できない。。。

そもそも前提としてなのですが、TwitterでOAuthを有効にするためにはTwitter API v2のアクセスレベルの変更を申請しなければならないようでした。現在の開発者アカウントが「 Essential access(エッセンシャルアクセス)」であれば「 Elevated access(高度なアクセス) 」に変更するようにします。

そして実装を進めるにあたり注意が必要なのですがTwitterログイン機能をドキュメントや他の参考記事等をそのままに実装するとユーザー情報を取得する際、以下のエラーで躓きます。

Type error: Argument 1 passed to League\OAuth1\Client\Server\Server::getTokenCredentials() must be an instance of League\OAuth1\Client\Credentials\TemporaryCredentials, null given, called in /vendor/laravel/socialite/src/One/AbstractProvider.php on line 113

多くのサイトでは以下の記述で書かれていることが多いのですが、なぜかこれでは動かず。。。
どうやらセッション周りで問題があるようですが、解決策が見つからず。。。

Socialite::driver($provider)->user();

以下のように、stateless()メソッドを書けばセッションの引き継ぎを行なってくれると書かれていますが、Twitterにはこのメソッド自体存在しないらしくundefinedが出てしまいます。。。

Socialite::driver($provider)->stateless()->user();

ということで解決策としてなくなくアクセストークンでユーザー情報を取得することにしました。。。

Socialite::driver($provider)->userFromTokenAndSecret(env('TWITTER_ACCESS_TOKEN'), env('TWITTER_ACCESS_TOKEN_SECRET'));

参考
https://blog.unasuke.com/2022/omniauth-twitter2/
https://qiita.com/nooboolean/items/7c524cb84bf5dc4f52c3
https://teratail.com/questions/124977

Response

Laravel Fortifyではログイン処理の最後にLoginResponseを返しますのでそれを参考にして編集します。

<?php

namespace App\Http\Responses;

use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Fortify;
use Illuminate\Validation\ValidationException;

class LoginResponse implements LoginResponseContract
{
    /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function toResponse($request)
    {
        $user = Auth::user();
        return $request->wantsJson()
            ? response()->json([$user, 'auth' => true], 200)
            : redirect()->intended(Fortify::redirects('login'));
    }
}

同様にLaravel Fortifyでは登録処理の最後はRegisterResponseを返しますのでそれを参考にして編集します。

<?php

namespace App\Http\Responses;

use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Illuminate\Support\Facades\Auth;


class RegisterResponse implements RegisterResponseContract
{
    /**
     * Create an HTTP response that represents the object.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function toResponse($request)
    {
        $user = Auth::user();
        return $request->wantsJson()
            ? response()->json([$user, 'auth' => true], 200)
            : redirect('/');
    }
}

Nuxt.js側

ログインページ

「Facebookでログイン」等のログインボタンを押すと以下のメソッドが発火します。
以下のメソッドではSNS認証用のリダイレクトURLが返ってくるので、window.location.hrefでそのURLにアクセスするようにします。

Laravel SanctumのSPA認証ではログイン処理の前に必ず/sanctum/csrf-cookieエンドポイントにリクエストを送信して、アプリケーションのCSRF保護を初期化する必要があるので以下を記述してます。

axios.get ( '/sanctum/csrf-cookie', { withCredentials: true } 
    const twitterLogin  = async() => {
      axios.get ( '/sanctum/csrf-cookie', { withCredentials: true } )
      await axios
        .get ( '/api/oauth/twitter' )
        .then ( ( response ) => {
          window.location.href = response.data.redirect_url
        } )
        .catch ( ( err ) => {
          console.log ( err )
          console.log ( err.response )
        } )
    }
    const facebookLogin = async() => {
      axios.get ( '/sanctum/csrf-cookie', { withCredentials: true } )
      await axios
        .get ( '/api/oauth/facebook' )
        .then ( ( response ) => {
          window.location.href = response.data.redirect_url
        } )
        .catch ( ( err ) => {
          console.log ( err )
          console.log ( err.response )
        } )
    }

SNS側からリダイレクトされるページを作成

認証の結果、Laravel側のほうで設定したようにprovider_idがあればそのままログインさせますが、なければ本登録を進めるページに遷移させるようにします。
メールアドレスはSNS間で使いまわしている恐れがありますので、登録画面で登録してもらうようにします。

SNS側からリダイレクトされるURLは先ほど設定したCALLBACK_URLになりますのでNuxtのルーティングに合うようにpages/oauth/(SNS名)/callbackにファイルを作成します。

すると「Facebookでログイン」等のボタンを押すと以下のページにリダイレクトされるようになりますので戻ってきたURLをそのままroute.value.queryで取得してaxiosでLaravel側に送信し、データを取得します。以下はfacebookのファイルを作成していますがTwitterとLINEも同じ手法で作成します。

登録画面では記述内容をSocialiteControllerのstoreメソッドに送ります。

<script>
import axios from 'axios'
import { ref, useRoute } from '@nuxtjs/composition-api'
export default {
  setup() {
    const route = useRoute()
    const registerForm = ref({
      name: '',
      email: '',
      provider_id: '',
    })

    const callbackData = async () => {
      axios.get('/sanctum/csrf-cookie', { withCredentials: true })
      await axios
        .get('/api/oauth/facebook/callback', { params: route.value.query })
        .then((response) => {
          console.log(response.data)
          if (response.data.auth == true) {
            //ユーザー情報があればTOPに遷移
            router.push('/')
          }
          registerForm.value.name = response.data.name
          registerForm.value.provider_id = response.data.id
        })
        .catch((err) => {
          console.log(err)
        })
    }
    callbackData()

    const register = async () => {
      axios.get('/sanctum/csrf-cookie', { withCredentials: true })
      await axios
        .post('/api/oauth/facebook/register', registerForm)
        .then((response) => {
          if (response.data.auth == true) {
            //登録成功ならばTOPに遷移
            router.push('/')
          }
        })
        .catch((err) => {
          console.log(err)
          console.log(err.response)
        })
    }

    return {
      registerForm,
      register,
    }
  },
}
</script>

まとめ

今回は簡単ではありますがLaravelとNuxtでSNS認証を実装しました。
本来であれば、取得した情報をVuexに保存したり、middlewareでログインしてるかどうかを判別する必要があったりと足りない部分も多いかと思いますが、いろいろつまりポイントがあって苦戦したので誰かの助けになればうれしいです。
また、Twitterの認証だけうまく取得できなくて困ったので解決策をご存じの方が
いらっしゃいましたら教えていただけると幸いです。

コメント

  1. 私も同様のエラーが発生したのでTwitterOAuthというライブラリを使用して実装しました。

    • コメントありがとうございます!TwitterOAuthというライブラリがあるのですね!
      今度実装する機会がありましたら試してみたいと思います!

  2. postリクエストの場合はaxios.get(‘/sanctum/csrf-cookie’)を叩くのはわかるのですが、getリクエストの場合もaxios.get(‘/sanctum/csrf-cookie’)を叩かなければいけないのですか??

    • 質問ありがとうございます!
      Laravel Sanctumを使用している場合、GETリクエストであってもaxios.get(‘/sanctum/csrf-cookie’)を叩く必要があると思っています。
      その後の非GETリクエスト(例えばOAuth認証の一部として行われる可能性があるPOSTリクエスト)でCSRF保護を有効にするためです。
      (間違っていたらすみません。。。)

コメントを残す

入力エリアすべてが必須項目です。メールアドレスが公開されることはありません。

内容をご確認の上、送信してください。