Laravel 5.3で外部アカウントを使用した認証機能を実装する方法

* LaravelではfacebookやtwitterのOAuth認証によるログイン機能を実現するためのパッケージ「laravel/socialite」が存在する。
* 日本国内向けのサービス(LINEやYahoo!JAPAN)を使った認証はサポートされていないが、これについては[こちらのポスト](http://qiita.com/LowSE01/items/25be61b1039e54bc722b)で。

## 追記
* 2017.05.19

> SocialController::createOrGetUser()において、各種サービスから取得したメールアドレスを保存していたが、make:authを使って作成する認証方式を併用する場合に、SNS登録メールアドレスとバッティングしてログインできなくなる事象があったため、該当箇所をコメントアウトしました。

## 前提
* php artisan make:authを実行済みであること

	> [Laravel 5.3で認証関連機能を実装する方法](http://qiita.com/LowSE01/items/0014ffea28e4adcada9d)

## 導入手順

### laravel/socialiteパッケージを取得

* composer requireでパッケージを取得する。

	``` console
	cd (/your/laravel/project/path)
	composer require laravel/socialite
	```

* config/app.phpを編集する

	``` config/app.php
	<?php
	
	return [
		...
		'providers'       => [
			...
			//laravel/socialite
			Laravel\Socialite\SocialiteServiceProvider::class,
			...
		],
		...
		'aliases'         => [
			...
			//laravel/socialite
			'Socialite'    => Laravel\Socialite\Facades\Socialite::class,
			...
		],
	
	];
	
	```
		
### socialiteの設定

* 各OAuthサービスごとに、ID,トークンや認証処理後のコールバックURLなどを指定する必要がある。
	* config/services.phpを編集する。
		
		``` config/services.php
		<?php
		
		return [
			...
			//Socialite認証
			'twitter'   => [
				'client_id'     => env('TWITTER_CLIENT_ID'),
				'client_secret' => env('TWITTER_CLIENT_SECRET'),
				'redirect'      => env('TWITTER_REDIRECT'),
			],
			'facebook'  => [
				'client_id'     => env('FACEBOOK_CLIENT_ID'),
				'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
				'redirect'      => env('FACEBOOK_REDIRECT'),
			],
			'google'    => [
				'client_id'     => env('GOOGLE_CLIENT_ID'),
				'client_secret' => env('GOOGLE_CLIENT_SECRET'),
				'redirect'      => env('GOOGLE_REDIRECT'),
			],
		];
			
		```						

	* .env.xxxxxを編集
		
		``` .env.xxxxx
		...
		TWITTER_CLIENT_ID=(twitter-client-id)
		TWITTER_CLIENT_SECRET=(twitter-client-secret)
		TWITTER_REDIRECT=(twitter-redirect-url)
		
		FACEBOOK_CLIENT_ID=(facebook-client-id)
		FACEBOOK_CLIENT_SECRET=(facebook-client-secret)
		FACEBOOK_REDIRECT=(facebook-redirect-url)
		
		GOOGLE_CLIENT_ID=(google-client-id)
		GOOGLE_CLIENT_SECRET=(google-client-secret)
		GOOGLE_REDIRECT=(google-redirect-url)
		...
		```

### テーブル&モデル作成

* 認証に使用するテーブルを作成する

	* migration作成

		``` console
		cd your/laravel/project/path
		php artisan make:migration create_social_accounts_table
		```

		``` database/migrations/xxxx_xx_xx_xxxxxx_social_accounts.php
		<?php
		
		use Illuminate\Support\Facades\Schema;
		use Illuminate\Database\Schema\Blueprint;
		use Illuminate\Database\Migrations\Migration;
		
		class CreateSocialAccountsTable extends Migration
		{
		    /**
		     * Run the migrations.
		     *
		     * @return void
		     */
		    public function up()
		    {
		        Schema::create('social_accounts', function (Blueprint $table) {
					$table->increments('id');
					$table->integer('user_id')->unsigned()->nullable();
					$table->string('provider_user_id');
					$table->mediumText('provider_access_token')->nullable();
					$table->string('provider');
					$table->timestamps();
					$table->softDeletes();
		        });
		    }
		
		    /**
		     * Reverse the migrations.
		     *
		     * @return void
		     */
		    public function down()
		    {
		        Schema::drop('social_accounts');
		    }
		}
		```
		

	* usersテーブルの定義修正
	
		``` php:database/migrations/2014_10_12_000000_create_users_table.php
		<?php
		
		// ...
		
		class CreateUsersTable extends Migration
		{
			// ...
		
				public function up()
			{
				Schema::create('users', function (Blueprint $table) {
					// ...	
					// $table->string('email')->unique();	// -	
					// $table->string('password');			// -
					$table->string('email')->nullable();	// +
					$table->string('password')->nullable();	// +
					// ...
				});
			}
			
			//...
		}
		```
		
		* 認証に使用するサービスによって、取得できる項目は違うため、このへんのnullable対応は必要に応じて実施する。

	* テーブル作成
		
		```	console
		cd your/laravel/project/path
		php artisan migrate:refresh
		```

* 認証に使用するテーブルのモデルを用意する。
	
	``` app/SocialAccount.php
	
	<?php
	
	namespace App;
	
	use Illuminate\Database\Eloquent\Model;
	
	class SocialAccount extends Model
	{
		protected $table = 'social_accounts';
	
		protected $primaryKey = 'id';
	
		/**
		 * The attributes that are mass assignable.
		 *
		 * @var array
		 */
		protected $fillable = [
			'user_id',
			'provider_user_id',
			'provider_access_token',
			'provider',
		];
	
		public function user()
		{
			return $this->belongsTo(User::class);
		}
	}
	
	```

### コントローラ作成

* 認証に使用するコントローラを作成する
	
	``` app/Http/Controllers/Auth/SocialController.php
	<?php
	namespace App\Http\Controllers\Auth;
	
	use App\Http\Controllers\Controller;
	use Illuminate\Http\Request;
	use Auth;
	use Socialite;
	
	use App\User;
	use App\SocialAccount;
	
	class SocialController extends Controller
	{
		protected $redirectTo = '/home';		//your-redirect-url-after-login
	
		// twitter
	
		public function getTwitterAuth()
		{
			return Socialite::driver('twitter')->redirect();
		}
	
		public function getTwitterAuthCallback()
		{
			$twitterUser = Socialite::driver('twitter')->user();
	
			$user = $this->createOrGetUser($twitterUser, 'twitter');
			Auth::login($user, true);
	
			return redirect($this->redirectTo);
		}
	
		// facebook
	
		public function getFacebookAuth()
		{
			return Socialite::driver('facebook')->redirect();
		}
	
		public function getFacebookAuthCallback()
		{
			$facebookUser = Socialite::driver('facebook')->stateless()->user(); // (1)
	
			$user = $this->createOrGetUser($facebookUser, 'facebook');
			Auth::login($user, true);
	
			return redirect($this->redirectTo);
		}
	
		// Google
	
		public function getGoogleAuth()
		{
			return Socialite::driver('google')->redirect();
		}
	
		public function getGoogleAuthCallback()
		{
			$googleUser = Socialite::driver('google')->user();
	
			$user = $this->createOrGetUser($googleUser, 'google');
			Auth::login($user, true);
	
			return redirect($this->redirectTo);
		}
	
		public function createOrGetUser($providerUser, $provider)
		{
			$account = SocialAccount::firstOrCreate([
				'provider_user_id' => $providerUser->getId(),
				'provider'         => $provider,
			]);
	
			if (empty($account->user))
			{
				$user = User::create([
					'name'   => $providerUser->getName(),
					// 'email'  => $providerUser->getEmail(), # 削除 (2017.05.19)
					'avatar' => $providerUser->getAvatar(),
				]);
				$account->user()->associate($user);
			}
	
			$account->provider_access_token = $providerUser->token;
			$account->save();
	
			return $account->user;
		}
	}
	```
	
### ルーティング設定

* routes/web.phpに追加

	``` routes/web.php
	...
	//twitter
	Route::get('/login/twitter', 'Auth\SocialController@getTwitterAuth');
	Route::get('/login/twitter/callback', 'Auth\SocialController@getTwitterAuthCallback');
	
	//facebook
	Route::get('/login/facebook', 'Auth\SocialController@getFacebookAuth');
	Route::get('/login/facebook/callback', 'Auth\SocialController@getFacebookAuthCallback');
	
	//google
	Route::get('/login/google', 'Auth\SocialController@getGoogleAuth');
	Route::get('/login/google/callback', 'Auth\SocialController@getGoogleAuthCallback');
	...
	```

### 導線作成
* どこかしらのビューに、認証用URLへの導線を張る

``` any.view.blade.php
...
(例)
<div class="container">
    <div class="btn-group btn-group-justified">
        <a class="btn btn-default" href="/login/facebook">
            facebook
        </a>
        <a class="btn btn-default" href="/login/twitter">
            twitter
        </a>
        <a class="btn btn-default" href="/login/google">
            google
        </a>
    </div>
</div>
...
```