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
Contents
実装方法
まず事前に以下のサイトからアプリケーションを作成する必要があります。
アプリケーションの作成方法は今回は割愛します。。。
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の認証だけうまく取得できなくて困ったので解決策をご存じの方が
いらっしゃいましたら教えていただけると幸いです。
私も同様のエラーが発生したのでTwitterOAuthというライブラリを使用して実装しました。