Laravelログイン機能に複数のSNS認証を追加する（socialite）


## socialiteでSNSログインを実装

Laravelで、SNSのアカウントを使ったログインの実装をした時のメモです。

### socialiteとは

[soscialite](https://laravel.com/docs/7.x/socialite)とは、Laravelで簡単にOAuth（SNS認証）を可能にするパッケージツールです。

はじめに、composerを使って`socialite`をインストールします。

```
$ composer require laravel/socialite
```

## モデル

各テーブルに紐付く`User.php`モデルと`IdentityProvider.php`のリレーションを設定します。

```
$ php artisan make:model IdentityProvider -m
```

### Userモデルの設定

ログイン時に新規作成するUserテーブルは、挿入できるように追記しておきます。

```php:App/User.php

class User extends Authenticatable
{
    use Notifiable;

     function IdentityProviders()
    {
        // IdentityProviderモデルと紐付ける 1対多の関係
        return $this->hasMany(IdentityProvider::class);
    }

```
ユーザーに対して各プロバイダーは「1対多」の関係になるので`hasMany`を設定します。


###IdentityProviderモデルの設定

```php:App/IdentityProvider.php
class IdentityProvider extends Model
{
    protected $fillable = ['user_id', 'provider_name', 'provider_id'];

    function user()
    {
        return $this->belongsTo(User::class);
    }
}
```

プロバイダーの情報はユーザーに属するので`belongsTo`を設定します。


### マイグレーションの設定

ソーシャルログインは、メールアドレスやパスワードがない場合があるので`nullable()`にしておきます。

```php:database/migrations/xxxx_xx_xx_xxxxxx_create_users_table.php
class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique()->nullable(); // 追記
            $table->string('password')->nullable(); // 追記
            $table->rememberToken();
            $table->timestamps();
        });
    }
```
![スクリーンショット 0032-05-29 18.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/aa40477b-a56a-708b-cb4d-c12fb921d56e.png)
usersに紐付けるプロバイダー情報のテーブルも作成します。
今回はテーブル名を`identity_providers`としました。

```php:database/migrations/xxxx_xx_xx_xxxxxx_create_Identity_Providers_table
class CreateIdentityProvidersTable extends Migration
{
    public function up()
    {
        Schema::create('identity_providers', function (Blueprint $table) {
            $table->unsignedBigInteger('user_id');
            $table->foreign('user_id')->references('id')->on('users'); // 外部キー制約
            $table->string('provider_id');
            $table->string('provider_name');
            $table->primary(['provider_name', 'provider_id']); // 複合キー
            $table->unique(['user_id', 'provider_name']); // ユニーク制限
            $table->timestamps();
        });
    }
```
`users`テーブルの`user_id`に`identity_providers`に`id`を紐付けて管理したいので、
`$table->foreign('user_id')->references('id')->on('users');`のように、
マイグレーションの外部キー制約を設定します。
![スクリーンショット 0032-05-29 18.06.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/b533e81a-938b-8715-5ad6-395be8160443.png)
## ルーティング

ユーザーを認証する準備ができたら、２つのルートを設定します。

1つはユーザをOAuthプロバイダにリダイレクトするためのもので、
もう1つは認証後にプロバイダからのコールバックを受け取るためのものです。

### web.phpの設定

`web.php`にてログイン画面でボタンをクリックしたときのリンクを設定します。
たとえば、シンプルにGitHubだけで実装する場合には下記のようになります。

```php:routes/web.php
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');

Route::get('login/github', 'Auth\LoginController@redirectToProvider');
Route::get('login/github/callback', 'Auth\LoginController@handleProviderCallback');
```

複数のプロバイダーで認証を実装する場合は、引数に変数を用いて変換します。

```php:routes/web.php
Route::get('/login/{provider}', 'Auth\LoginController@redirectToProvider')->where('social', 'github|google|facebook');
Route::get('/login/{provider}/callback', 'Auth\LoginController@handleProviderCallback')->where('social', 'github|google|facebook');
```
### Controllerの設定
設定した情報からSNS認証ログインを実装します。

```php:App/Http/Controllers/Auth/LoginController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;

use Auth;
use Socialite;
use App\User;
use App\IdentityProvider;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = '/home';

    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    public function redirectToProvider($social)
    {
        return Socialite::driver($social)->redirect();
    }

    public function handleProviderCallback($provider)
    {
        try {
            $user = Socialite::driver($provider)->user();
        } catch (Exception $e) {
            return redirect('/login');
        }

        $authUser = $this->findOrCreateUser($user, $provider);
        Auth::login($authUser, true);
        return redirect($this->redirectTo);
    }

    public function findOrCreateUser($providerUser, $provider)
    {
        $account = IdentityProvider::whereProviderName($provider)
                    ->whereProviderId($providerUser->getId())
                    ->first();

        if ($account) {
            return $account->user;
        } else {
            $user = User::whereEmail($providerUser->getEmail())->first();

            if (!$user) {
                $user = User::create([
                    'email' => $providerUser->getEmail(),
                    'name'  => $providerUser->getName(),
                ]);
            }

            $user->IdentityProviders()->create([
                'provider_id'   => $providerUser->getId(),
                'provider_name' => $provider,
            ]);

            return $user;
        }
    }
}
```

`redirectToProvider（）`メソッドは、ユーザーをOAuthプロバイダーに送信します。
`handleProviderCallback（）`メソッドは、プロバイダーからユーザーの情報を取得します。

`findOrCreateUser（）`メソッドは、ユーザーがまだ存在しない場合にユーザーを作成して、
プロバイダーから取得した情報を`identity_providers`テーブルに登録します。

## コンフィグレーション

`Socialite`を使用する前に、アプリケーションが使用するOAuthサービスの認証情報を設定します。

### services.phpの設定

認証情報を必要とする各プロバイダーの設定を行います。

```php:config/services.php
return [
    'github' => [
        'client_id' => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect' => env('APP_URL') . '/login/github/callback',
    ],

    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('APP_URL') . '/login/google/callback',
    ],

    'facebook' => [
        'client_id' =>  env('FACEBOOK_APP_ID'),
        'client_secret' => env('FACEBOOK_APP_SECRET'),
        'redirect' =>  env('APP_URL') . '/login/facebook/callback',
    ],
];
```
`app.php`でsocialiteの設定を行います。

```php:config/app.php
    'providers' => [
        // 追加
        Laravel\Socialite\SocialiteServiceProvider::class,
    ],

    'aliases' => [
        // 追加
        'Socialite' => Laravel\Socialite\Facades\Socialite::class,
    ],
```

### .envの設定
.envファイルにて、各プロバイダーで取得したクライアントIDとシークレットIDを設定します。
- [GitHub](https://github.com/settings/developers)
- [Google](https://console.developers.google.com)
- [Facebook](https://developers.facebook.com/apps/)

```:.env
GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

GOOGLE_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

FACEBOOK_APP_ID=xxxxxxxxxxxxxxx
FACEBOOK_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## Viewの設定
ログイン画面のテンプレートに追記します。
（※下記の例では、Bootstrapを使用しています。）

```php:login.blade.php
    <div class="form-group row mt-5">
        <label for="name" class="col-sm-4 col-form-label text-md-right">SNSログイン</label>
        <div class="col-md-6">
            <a href="{{ url('login/google')}}" class="btn btn-danger"><i class="fa fa-google"> Google</i></a>
            <a href="{{ url('login/facebook')}}" class="btn btn-primary"><i class="fa fa-facebook"> Facebook</i></a>
            <a href="{{ url('login/github')}}" class="btn btn-secondary"><i class="fa fa-github"> GitHub</i></a>
        </div>
    </div>
```

デフォルトのログインテンプレートに追記すると、このように表示されます。
![スクリーンショット 0032-05-23 8.16.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/b37c41e3-3942-40d4-9151-68392e686a40.png)

これで、各プロバイダーの情報を使ってログインができるようになります。

## 参考
- [Laravel Socialite](https://laravel.com/docs/7.x/socialite)
- [一番分かりやすいOAuthの説明](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)