【Laravel】Socialiteでソーシャルログイン【Google / Twitter】

# 0. はじめに
大阪のLaravel初学者サウナーこと、kazumakishimoto([@kazuma_dev](https://twitter.com/kazuma_dev))です！
`Laravel Socialite`の`Google / Twitter`ソーシャルログインです！
※AWSverは下記記事参照

https://qiita.com/kazumakishimoto/items/fd4afc02f9490ddb89e5

## 0-1. 全体の流れ
> [1.Google](#1-google)
> [2.Twitter](#2-twitter)
> [3.Laravel](#3-laravel)
> [補足](#補足)
> [Reference](#reference)

## 0-2. 本記事の対象者
- `Laravel Socialite`の`Google / Twitter`ソーシャルログイン

## 0-3. 事前準備
- `Google / Twitter`アカウント作成済み(※TwitterAPIは申請が必要)
- `grfl`や`hoge`はサンプル名なので適宜変更して下さい

https://console.developers.google.com/project

https://developer.twitter.com/en

## 0-4. 要件
- GoogleAPI連携
- TwitterAPI連携
- Laravel Socialite実装

## 0-5. 使用画像のイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/3574dd02-c892-4fdf-f604-75ca8c120296.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/e6514032-a5ab-3b38-54f0-1365504d23f6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/ded6532a-2b70-f3f7-2a9c-097b6f2f0e64.png)

# 1. Google
https://console.developers.google.com/project

## 1-1. プロジェクト作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/071ea629-5769-f5ee-d210-448ce2554fa0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/cec7fd2d-0f02-3701-47b3-94a860053c04.png)

## 1-2. OAuth同意画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/a9139fee-4ae2-a0ba-738e-3900d95217c8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/3fd2759c-a87b-0450-244f-6e5a8773ed6d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/43978bad-0142-a58b-9516-d55990d7ae3a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/2005113c-b7a9-0883-4a11-0a7e9a8e9743.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/cbbaa3b8-093e-6a3b-eb1a-3f6a92f3edf1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/893dc50f-de72-f242-bb98-1e462e53d8bd.png)

## 1-3. OAuthクライアントID
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/435466f1-b904-2156-1e35-58066b0df2d4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/c0ad1fa0-ed93-2e8e-4681-567695377ce2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/085bdf12-6662-3c45-d593-f6fbe77cc6d6.png)


# 2. Twitter
https://developer.twitter.com/en

## 2-1. TwitterAPI申請
- 下記記事を参照にTwitterAPI申請する

https://uedive.net/2021/5471/laravel8twitterlogin/

https://dev.classmethod.jp/articles/twitter-api-approved-way/

https://qiita.com/QrioYagi/items/6afcbfb7193562921ee6

## 2-2. アプリ作成
- APIキーやトークンは後ほど変わるためコピーせず進む
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/b4d6fe61-ef91-07fd-cc4c-d8b3353ba265.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/7b72d9a4-1057-4a00-e9d1-60a2041ece5c.png)

## 2-3. OAuth設定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/b4c06e75-55d8-42ce-3cbe-19849494596c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/ac436a25-b564-c035-c676-f95dcd10a48d.png)

## 2-4. APIキー/トークン発行
- `.env`ファイルに記載するためコピーしておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687994/bc2e7d4f-be8f-a9b3-781d-591404d7206c.png)


# 3. Laravel
## 3-1. Laravel Socialite
```terminal:local
$ docker-compose exec app bash
# COMPOSER_MEMORY_LIMIT=-1 composer require laravel/socialite
Package manifest generated successfully.
# exit
```

## 3-2. config
### 3-2-1. app.php
```php:config/app.php
'providers' => [ 
    // 略
    Laravel\Socialite\SocialiteServiceProvider::class,
],
'aliases' => [
    // 略
    'Socialite' => Laravel\Socialite\Facades\Socialite::class,
]
```

### 3-2-2. services.php
```php:config/services.php
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('APP_URL') . '/login/google/callback',
    ],

    'twitter' => [
        'client_id' => env('TWITTER_CLIENT_ID'),
        'client_secret' => env('TWITTER_CLIENT_SECRET'),
        'redirect' => env('APP_URL') . '/login/twitter/callback',
    ],
```

## 3-3. Route
```php:web.php
# SNSアカウントログイン
Route::prefix('login')->name('login.')->group(function () {
    Route::get('/{provider}', 'Auth\LoginController@redirectToProvider')->name('{provider}');
    Route::get('/{provider}/callback', 'Auth\LoginController@handleProviderCallback')->name('{provider}.callback');
});
# SNSアカウントユーザー登録
Route::prefix('register')->name('register.')->group(function () {
    Route::get('/{provider}', 'Auth\RegisterController@showProviderUserRegistrationForm')->name('{provider}');
    Route::post('/{provider}', 'Auth\RegisterController@registerProviderUser')->name('{provider}');
});
```

## 3-4. Migration
```php:database/migrations/2014_10_12_000000_create_users_table.php
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('twitter_id')->nullable();
            $table->string('name')->unique();
            $table->string('age')->nullable();
            $table->string('gender')->nullable();
            $table->string('avatar')->nullable();
            $table->text('introduction')->nullable();
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }
```

## 3-5. Model
```php: app/Models/User.php
    protected $fillable = [
        'twitter_id', 'name', 'age', 'gender', 'avatar', 'introduction', 'email', 'password',
    ];
```

## 3-6. Controller
### 3-6-1. LoginController.php
```php:app/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    // ゲストユーザー用のユーザーIDを定数として定義
    private const GUEST_USER_ID = 1;

    // ゲストログイン処理
    public function guestLogin()
    {
        // id=1 のゲストユーザー情報がDBに存在すれば、ゲストログインする
        if (Auth::loginUsingId(self::GUEST_USER_ID)) {
            return redirect('/');
        }

        return redirect('/');
    }

    // SNS認証ページへユーザーをリダイレクト
    public function redirectToProvider(string $provider)
    {
        return Socialite::driver($provider)->redirect();
    }

    // ログイン
    public function handleProviderCallback(Request $request, string $provider)
    {
        // 認証結果の受け取り
        $providerUser = Socialite::driver($provider)->user();

        // Google
        if ($provider === 'google') {
            // Googleから取得したユーザー情報からメールアドレスを取得
            $user = User::where('email', $providerUser->getEmail())->first();

        // Twitter
        } elseif ($provider === 'twitter') {
            // Twitterから取得したユーザー情報からユーザーIDを取得
            $user = User::where('twitter_id', $providerUser->getId())->first();
        }

        // ログイン処理
        if ($user) {
            $this->guard()->login($user, true);
            return $this->sendLoginResponse($request);
        }

        // Google
        if ($provider === 'google') {

            $data = [
                'provider' => $provider,
                'email' => $providerUser->getEmail(),
                'token' => $providerUser->token,
            ];

            return redirect()->route('register.{provider}', $data);

        // Twitter
        } elseif ($provider === 'twitter') {

            $data = [
                'provider' => $provider,
                'twitter_id' => $providerUser->getId(),
                'token' => $providerUser->token,
                'tokenSecret' => $providerUser->tokenSecret,
            ];
            // DBにユーザー情報がなければ作成する
            return redirect()->route('register.{provider}', $data);
        }
    }
}
```

### 3-6-2. RegisterController.php
```php:app/Http/Controllers/Auth/RegisterController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Socialite\Facades\Socialite;
use App\Rules\CustomPasswordValidation;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = RouteServiceProvider::HOME;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        $messages = [
            'regex' => ':attributeに「/」と半角スペースは使用できません。',
        ];

        return Validator::make($data, [
            'name' => ['required', 'regex:/^(?!.*\s).+$/u', 'regex:/^(?!.*\/).*$/', 'max:15', 'unique:users'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', new CustomPasswordValidation, 'confirmed'],
        ],
        $messages
    );
    }

    public function showProviderUserRegistrationForm(Request $request, string $provider)
    {
        $token = $request->token;

        $providerUser = Socialite::driver($provider);

        // Google
        if ($provider === 'google') {
            $providerUser = $providerUser->userFromToken($token);

            $data = [
                'provider' => $provider,
                'email' => $providerUser->getEmail(),
                'token' => $providerUser->token,
            ];

            return view('auth.social_register', $data);

        // Twitter
        } elseif ($provider === 'twitter') {
            $tokenSecret = $request->tokenSecret;
            $providerUser = $providerUser->userFromTokenAndSecret($token, $tokenSecret);

            $data = [
                'provider' => $provider,
                'twitter_id' => $providerUser->getId(),
                'token' => $providerUser->token,
                'tokenSecret' => $providerUser->tokenSecret,
            ];

            return view('auth.social_register', $data);
        }
    }

    public function registerProviderUser(Request $request, string $provider)
    {
        // Google
        if ($provider === 'google') {
            $request->validate([
                'name' => ['required', 'string', 'min:1', 'max:15', 'unique:users'],
                'token' => ['required', 'string'],
            ]);

        // Twitter
        } elseif ($provider === 'twitter') {
            $request->validate([
                'name' => ['required', 'string', 'min:1', 'max:15', 'unique:users'],
                'token' => ['required', 'string'],
                'tokenSecret' => ['required', 'string'],
            ]);
        }

        $token = $request->token;

        $providerUser = Socialite::driver($provider);

        // Google
        if ($provider === 'google') {
            $providerUser = $providerUser->userFromToken($token);

        // Twitter
        } elseif ($provider === 'twitter') {
            $tokenSecret = $request->tokenSecret;
            $providerUser = $providerUser->userFromTokenAndSecret($token, $tokenSecret);
        }

        // Google
        if ($provider === 'google') {
            $user = User::create([
                'name' => $request->name,
                'avatar' => asset(config('user.avatar_path.default')),
                'email' => $providerUser->getEmail(),
                'password' => null,
            ]);

        // Twitter
        } elseif ($provider === 'twitter') {
            $user = User::create([
                'name' => $request->name,
                'avatar' => asset(config('user.avatar_path.default')),
                'email' => $request->email,
                'twitter_id' => $providerUser->getId(),
                'password' => null,
            ]);
        }

        $this->guard()->login($user, true);

        return $this->registered($request, $user)
        ?: redirect($this->redirectPath());
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        return User::create([
            'name' => $data['name'],
            'avatar' => asset(config('user.avatar_path.default')),
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}

```

## 3-7. View
### 3-7-1. login.blade.php
```php:resources/views/auth/login.blade.php
<div>
    <a href="{{ route('login.{provider}', ['provider' => 'google']) }}" class="btn btn-block btn-danger mb-4 col-lg-8 col-md-9 col-sm-10 col-xs-12"><i class="fab fa-google mr-1"></i>Googleでログイン</a>
</div>
<div>
    <a href="{{ route('login.{provider}', ['provider' => 'twitter']) }}" class="btn btn-block btn-info mb-4 col-lg-8 col-md-9 col-sm-10 col-xs-12"><i class="fab fa-twitter mr-1"></i>Twitterでログイン</a>
</div>
```

### 3-7-2. register.blade.php
```php:resources/views/auth/register.blade.php
<div>
    <a href="{{ route('login.{provider}', ['provider' => 'google']) }}" class="btn btn-block btn-danger mb-4"><i class="fab fa-google mr-1"></i>Googleで登録</a>
</div>
<div>
    <a href="{{ route('login.{provider}', ['provider' => 'twitter']) }}" class="btn btn-block btn-info mb-4"><i class="fab fa-twitter mr-1"></i>Twitterで登録</a>
</div>
```

### 3-7-3. social_register.blade.php
```php:resources/views/auth/social_register.blade.php
@extends('app')

@section('title', 'ユーザー登録 - grfl')

@section('content')

@include('nav')
<main>
    <div class="container">
        <div class="row">
            <div class="mx-auto col col-12 col-sm-11 col-md-9 col-lg-7 col-xl-6">
                <div class="card mt-3">
                    <div class="card-body text-center">
                        <h2 class="h3 card-title text-center mt-2">ユーザー登録</h2>
                        @include('error_card_list')
                        <div class="card-text">
                            <form method="POST" action="{{ route('register.{provider}', ['provider' => $provider]) }}">
                                @csrf
                                <input type="hidden" name="token" value="{{ $token }}">
                                @if ($provider === 'twitter')
                                <input type="hidden" name='tokenSecret' value="{{ $tokenSecret }}">
                                @endif
                                <div class="md-form">
                                    <label for="name">ユーザー名</label>
                                    <input class="form-control" type="text" id="name" name="name" required>
                                    <small>英数字3〜16文字(登録後の変更はできません)</small>
                                </div>
                                <div class="md-form">
                                    <label for="email">メールアドレス</label>
                                    @if ($provider === 'google')
                                    <input class="form-control" type="text" id="email" name="email" value="{{ $email }}" disabled>
                                    @elseif ($provider === 'twitter')
                                    <input class="form-control" type="text" id="email" name="email" required>
                                    @endif
                                </div>
                                <button class="btn btn-block blue-gradient mt-2 mb-2" type="submit">ユーザー登録</button>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>

@include('footer')

@endsection

```

## 3-8. 環境変数
```terminal:ec2-user
$ ssh ec2-user@xx.xxx.xxx.xxx -i ~/.ssh/hoge.pem
$ cd /var/www/grfl/src
$ sudo vi .env
```
```diff:.env
# Google Auth
+ GOOGLE_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
+ GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

# Twitter Auth
+ TWITTER_CLIENT_ID=TwitterAPI Key
+ TWITTER_CLIENT_SECRET=TwitterAPI Secret Key
+ TWITTER_CALLBACKURL=https://grfl.work/login/twitter/callback
```
```terminal:ec2-user
$ php artisan config:cache
```

https://qiita.com/TakaKun/items/535bae987da54f5892d1

https://qiita.com/saku_tto_chan/items/724f00d6495deaceabaf

# 補足
## 開発環境（FW/ツールのバージョンなど）
|ツール|バージョン|
|:--|:--|
|Vue.js|2.6.14|
|jQuery|3.4.1|
|PHP|7.4.1|
|Laravel|6.20.43|
|MySQL|5.7.36|
|Nginx|1.18.0|
|Composer|2.0.14|
|npm|6.14.6|
|Git|2.33.1|
|Docker|20.10.11|
|docker-compose|v2.2.1|
|PHPUnit|8.0|
|CircleCI|2.1|
|heroku|7.59.4|
|MacBook Air|M1,2020|
|macOS|Monterey 12.3|
|Homebrew|3.3.8|

## ディレクトリ構造
```
【ルートディレクトリ】
├─ .circleci
│   └─ config.yml
├─ aws / CloudFormation
│   └─ ec2.yml
├─ docker
│   └─ mysql
│   └─ nginx
│   └─ php
│   └─ phpmyadmin
├─ src
│   └─ 【Laravelのパッケージ】
└─ docker-compose.yml
```

# Reference
https://www.techpit.jp/courses/11

https://www.techpit.jp/courses/78

https://console.developers.google.com/project

https://developer.twitter.com/en

https://qiita.com/NULL_000000/items/646af3c5a4f6eebca742

https://qiita.com/tamakiiii/items/cd027580e1d86e2652d7

https://zenn.dev/naoki_oshiumi/articles/80ab178d958447

https://uedive.net/2021/5471/laravel8twitterlogin/

https://dev.classmethod.jp/articles/twitter-api-approved-way/

https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be

https://qiita.com/TakaKun/items/535bae987da54f5892d1

https://qiita.com/saku_tto_chan/items/724f00d6495deaceabaf