Laravel5.7でソーシャルログイン（Socialite利用）


## やりたいこと

* いわゆるソーシャルログイン
* とりあえずFacebook（とTwitter）に対応したい

## 方針

make:authで生成した各種ファイルを利用

```bash
php artisan make:auth
```

## 準備

当然のことながらLaravelをインストール。.envの設定を済ませておく。

### socialiteのインストール

```bash
composer require laravel/socialite
```

### その他の設定

providers登録等はいらない（恐らくは5.5からのAuto dicoveryのおかげ）。

## 実装

以下のような感じでソーシャルログインを実装する。

* SNSログインボタンを設置
* SNSで認証＋ユーザー情報を取得する（emailの取得が必須）
* 該当のemailがなければ登録後、ログイン（1回目）
* 該当のemailがあればそのままログイン（2回目以降）

## Model

モデルとしてはUser(usersテーブル)を利用するが、標準ではpasswordが必須になっているのでマイグレーションファイルでnullを許可しておく。
（ソーシャルログインでは自社サービスとしてはpasswordを要求しないので）

```diff:database/migrations/xxxx_create_users_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
+           $table->string('password')->nullable();
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
}

```

マイグレート実行。

```bash
php artisan migrate
```

## Controller

処理はControllerに記述します。以下は最低限度の実装。
ソーシャルログインボタンからのリンク処理とCallback処理の２つ。

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

namespace App\Http\Controllers\Auth;

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

use Socialite;
use App\User;
use Auth;

class LoginController extends Controller
{
    use AuthenticatesUsers;

    protected $redirectTo = '/home';

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

    //ログインボタンからリンク
    public function socialLogin($social)
    {
        return Socialite::driver($social)->redirect();
    }

    //Callback処理
    public function handleProviderCallback($social)
    {
        //ソーシャルサービス（情報）を取得
        $userSocial = Socialite::driver($social)->stateless()->user();
        //emailで登録を調べる
        $user = User::where(['email' => $userSocial->getEmail()])->first();

        //登録（email）の有無で分岐
        if($user){

            //登録あればそのままログイン（2回目以降）
            Auth::login($user);
            return redirect('/home');

        }else{

            //なければ登録（初回）
            $newuser = new User;
            $newuser->name = $userSocial->getName();
            $newuser->email = $userSocial->getEmail();
            $newuser->save();

            //そのままログイン
            Auth::login($newuser);
            return redirect('/home');
        
        }
    }
}
```

## Route

必要なルートを定義します。上記メソッドに対応したログインボタンからのリンクとCallbackのURLを定義。

```diff:route.php
<?php

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index');

+//ログインボタンからのリンク
+Route::get('/login/{social}', 'Auth\LoginController@socialLogin')->where('social', 'facebook|twitter');
+//コールバック用
+Route::get('/login/{social}/callback', 'Auth\LoginController@handleProviderCallback')->where('social', 'facebook|twitter');
```

## View

大げさなコードを貼り付けていますが、基本はlogin.blade.phpの一部にログインボタンとリンクを追加しただけ。
ボタンの引数にfacebookとかtwitterとか渡してます。

```diff:login.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Login') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('login') }}">
                        @csrf

+                       <div class="form-group row">
+                           <label for="name" class="col-sm-4 col-form-label text-md-right">Login With</label>
+                           <div class="col-md-6">
+                               <a href="{{ url('login/facebook')}}" class="btn btn-social-icon btn-facebook"><i class="fa fa-facebook"></i></a>
+                               <a href="{{ url('login/twitter')}}" class="btn btn-social-icon btn-twitter"><i class="fa fa-twitter"></i></a>
+                           </div>
+                       </div>

                        <div class="form-group row">
                            <label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>

                                @if ($errors->has('email'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('email') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>

                                @if ($errors->has('password'))
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $errors->first('password') }}</strong>
                                    </span>
                                @endif
                            </div>
                        </div>

                        <div class="form-group row">
                            <div class="col-md-6 offset-md-4">
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>

                                    <label class="form-check-label" for="remember">
                                        {{ __('Remember Me') }}
                                    </label>
                                </div>
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Login') }}
                                </button>

                                <a class="btn btn-link" href="{{ route('password.request') }}">
                                    {{ __('Forgot Your Password?') }}
                                </a>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

あと、Facebookとかのアイコンを利用するためにfont-awesomeを導入しておきます。
共通layoutを定義しているapp.blade.phpの\<head></head>部分の最後に入れておきます。

```diff:app.blade.php
<head>
.
.
+<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
```

## config/services.phpと.envの設定

そもそもsocialiteの動作のためには、各SNSサービスのIDやSecret、Callback用のURLが必要になります。
各サービスの開発者向けサイトに行って、取得しておきます。

### config/services.php

外部サービスに関する情報はservices.phpに記述します。以下を追記します。
実際の値は.evnから取得するようにしておきます。

```php
    'facebook' => [
        'client_id' => env('FACEBOOK_API_ID'),
        'client_secret' => env('FACEBOOK_API_SECRET'),
        'redirect' => env('FACEBOOK_CALLBACKURL'),
    ],
```

### .env

上記に対応する設定を記述します。

CallbackのURLは下記でngrokで取得した URL+/login/facebook/callback とします。

```
FACEBOOK_API_ID=23792659xxxxxxxx
FACEBOOK_API_SECRET=9526653842f242201d157402xxxxxxxx
FACEBOOK_CALLBACKURL=https://c3fxxxxx.ngrok.io/login/facebook/callback
```

## Facebook開発者サイトでの情報取得・登録

* [facebook](https://developers.facebook.com/?locale=ja_JP)
* [twitter](https://developer.twitter.com/)


Callback URLは.envで設定したものと同じである必要があります。

## テスト

最近のソーシャルログインや各種アプリ開発は開発環境にもhttpsを要求してきます。要はlocalhostじゃテストしにくい状況となっています。
それを解決するためにngrokを利用します。ngrokはローカルURLをグローバルURLにマップしてくれます。

[こちら](https://qiita.com/kent_ear/items/6fcc477392aaede175f4)が大変参考になります。

### ngrokのインストール


```bash
brew cask install ngrok
```

### ngrokの起動

Laravel(php artisan serve)を利用してるので、8000を指定して起動します。

```bash
ngrok http 8000
```

### 動作確認

https://hostname/login からソーシャルボタンをクリックしてログインできるか確認します。