Socialiteを使ってLaravelで簡単にソーシャルログインを実装する方法をテスト付きで紹介したいと思います。
ソースはこちら
※ 上記ソースを clone
する場合は php artisan key:generate
を実行してください。
環境
Dockerで環境を用意しています。
- Debian 9.8
- PHP 7.3.4
- Laravel 5.8.13
- Socialite 4.1.3
- PHPUnit 7.5.9
準備
Laravelのインストール手順などは省略します。
もし環境が整っていないのであれば、以下を参考にLaravelのプロジェクトを作成してください。
https://readouble.com/laravel/5.8/ja/installation.html
クライアントIDとシークレットを取得する
Socialiteが対応しているサービスは以下
- GitHub
- GitLab
- Bitbucket
それぞれのサービスに登録してクライアントID、シークレットを取得してください。
今回はGoogleを利用します。
Google API Consoleにアクセスします。
https://console.developers.google.com/project
プロジェクトを作成
をクリックしてプロジェクトを作成します。
プロジェクトのダッシュボードへ移動します。
左上のハンバーガーアイコンをクリック
APIとサービス > 認証情報 > 認証情報を作成 > OAuth クライアント ID
でクライアントIDを作成します。
同意画面の設定を入力します。※ドメインやプライバシーポリシーのURLが必要です。(ドメインでホストされているURL)
前の画面へ戻り、アプリケーションの種類 > ウェブアプリケーション
を選択
承認済みのリダイレクトURI
を入力して保存
クライアントIDとシークレットが発行されます。(認証情報画面でいつでも確認可)
アプリケーションにクライアントIDとシークレットを設定する
各ファイルに以下を追加
# 末尾でOK
GOOGLE_CLIENT_ID=上記で取得したID
GOOGLE_CLIENT_SECRET=上記で取得したシークレット
GOOGLE_CALLBACK_URL=承認済みのリダイレクトURIを入れてください
return [
/**
* 省略
*/
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_CALLBACK_URL'),
],
];
Socialiteをインストール
$ composer require laravel/socialite
usersテーブルのmigrationファイルを編集する
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
// 追加
$table->string('provider_id')->nullable();
// 追加
$table->string('provider_name')->nullable();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
// null許容に変更
$table->string('password')->nullable();
$table->rememberToken();
$table->timestamps();
// 複合ユニークキー
$table->unique(['provider_id', 'provider_name']);
});
}
/**
* 以下省略
*/
}
上記では、各サービスから受け取るIDを格納するprovider_id
カラムと、ログインに利用したサービス名を格納するprovider_name
カラムを追加しています。
さらに、ソーシャルログインを利用する場合にpassword
は不要になるため、null
許容に設定します。
DBにテーブルを作成する
$ php artisan migrate
PHPUnitを設定する
アプリケーションが使うDBを使用するわけには行かないので、テスト用のDBを読み込ませます。
今回はインメモリなsqlite
でテストします。
もちろんMySql(MariaDB)
、PostgreSQL
を設定しても構いません。
return [
/**
* 省略
*/
'connections' => [
/**
* 省略
*/
// 追加
'testing' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
],
/**
* 以下省略
*/
];
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<!-- 省略 -->
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<!-- 追加 -->
<server name="DB_CONNECTION" value="testing"/>
</php>
</phpunit>
以上で準備は完了です。
実装
コントローラークラスを作成する
app/Http/Controllers/Auth/
直下に作成します。
$ php artisan make:controller Auth/OAuthController
作成したコントローラーをひとまず以下のように編集します。
use App\Http\Controllers\Controller;
class OAuthLoginController extends Controller
{
/**
* 各SNSのOAuth認証画面にリダイレクトして認証
* @param string $provider サービス名
* @return mixed
*/
public function socialOAuth(string $provider)
{
// TODO あとで実装
}
/**
* 各サイトからのコールバック
* @param string $provider サービス名
* @return mixed
*/
public function handleProviderCallback($provider)
{
// TODO あとで実装
}
}
ルーティングを設定する
Route::prefix('auth')->middleware('guest')->group(function() {
Route::get('/{provider}', 'Auth\OAuthController@socialOAuth')
->where('provider','google')
->name('socialOAuth');
Route::get('/{provider}/callback', 'Auth\OAuthController@handleProviderCallback')
->where('provider','google')
->name('oauthCallback');
});
ルーティングが正しく設定されているか確認します。
auth/{provider}
auth/{provider}/callback
が設定されていればOKです。
※Googleのコンソールで入力する承認済みのリダイレクトURI
はauth/{provider}/callback
です。
例: https://localhost/auth/google/callback
$ php artisan route:list
+--------+----------+--------------------------+------------------+------------------------------------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+--------------------------+------------------+------------------------------------------------------------------------+--------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api,auth:api |
| | GET|HEAD | auth/{provider} | socialOAuth | App\Http\Controllers\Auth\OAuthController@socialOAuth | web,guest |
| | GET|HEAD | auth/{provider}/callback | oauthCallback | App\Http\Controllers\Auth\OAuthController@handleProviderCallback | web,guest |
| | GET|HEAD | home | home | App\Http\Controllers\HomeController@index | web,auth |
| | GET|HEAD | login | login | App\Http\Controllers\Auth\LoginController@showLoginForm | web,guest |
| | POST | login | | App\Http\Controllers\Auth\LoginController@login | web,guest |
| | POST | logout | logout | App\Http\Controllers\Auth\LoginController@logout | web |
| | POST | password/email | password.email | App\Http\Controllers\Auth\ForgotPasswordController@sendResetLinkEmail | web,guest |
| | GET|HEAD | password/reset | password.request | App\Http\Controllers\Auth\ForgotPasswordController@showLinkRequestForm | web,guest |
| | POST | password/reset | password.update | App\Http\Controllers\Auth\ResetPasswordController@reset | web,guest |
| | GET|HEAD | password/reset/{token} | password.reset | App\Http\Controllers\Auth\ResetPasswordController@showResetForm | web,guest |
| | GET|HEAD | register | register | App\Http\Controllers\Auth\RegisterController@showRegistrationForm | web,guest |
| | POST | register | | App\Http\Controllers\Auth\RegisterController@register | web,guest |
+--------+----------+--------------------------+------------------+------------------------------------------------------------------------+--------------+
テストクラスを作成する
php artisan make:test OAuthTest
use Tests\TestCase;
class OAuthTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$this->providerName = 'google';
}
/**
* @test
*/
public function Googleの認証画面を表示できる()
{
// URLをコール
$this->get(route('socialOAuth', ['provider' => $this->providerName]))
->assertStatus(200);
}
/**
* @test
*/
public function Googleアカウントでユーザー登録できる()
{
// URLをコール
$this->get(route('oauthCallback', ['provider' => $this->providerName]))
->assertStatus(200);
}
}
特に解説は不要だと思います。
単純に各URLにGETリクエストしてその応答が成功かどうかを判定しているだけです。
ひとまずこの状態でテストを実行します。
以下のように出力されるはずです。
$ ./vendor/bin/phpunit --testdox tests/Feature/OAuthTest.php
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.
Feature\OAuth
✔ Googleの認証画面を表示できる
✔ Googleアカウントでユーザー登録できる
Time: 3.58 seconds, Memory: 16.00 MB
OK (2 tests, 2 assertions)
Googleの認証画面を表示させる
まずテストを修正します
use Tests\TestCase;
class OAuthTest extends TestCase
{
/**
* 省略
*/
/**
* @test
*/
public function Googleの認証画面を表示できる()
{
// URLをコール
$response = $this->get(route('socialOAuth', ['provider' => $this->providerName]));
$response->assertStatus(302);
$target = parse_url($response->headers->get('location'));
// リダイレクト先ドメインの検証
$this->assertEquals('accounts.google.com', $target['host']);
// パラメータの検証
$query = explode('&', $target['query']);
$this->assertContains('redirect_uri=' . urlencode(config('services.google.redirect')), $query);
$this->assertContains('client_id=' . config('services.google.client_id'), $query);
}
/**
* 以下省略
*/
}
コメントにある通りです。
- ステータスコードが302(リダイレクト)か
- リダイレクト先のドメインが
accounts.google.com
か - リダイレクトURLやクライアントIDは期待通りの設定がされているか
を検証します。
この状態でPHPUnit
を実行するとテストは失敗するはずです。
※リダイレクト先のドメインを検証する方法もっとスマートな方法ないでしょうか......
2019/11/29 修正
parse_url
を使って host
を取得する方法に切り替えました。
また、パラメータに付与されている値の検証も追加
コントローラーを修正します
Socialite
ファサードを使い、各サービスの認証画面にリダイレクトします。
Socialite::driver
の引数にはサービス名を渡します。
// 追加
use Socialite;
class OAuthLoginController extends Controller
{
/**
* 各SNSのOAuth認証画面にリダイレクトして認証
* @param string $provider サービス名
* @return mixed
*/
public function socialOAuth(string $provider)
{
return Socialite::driver($provider)->redirect();
}
/**
* 以下省略
*/
}
テストを実行します
今度はすべてのテストをパスするはずです。
./vendor/bin/phpunit --testdox tests/Feature/OAuthTest.php
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.
Feature\OAuth
✔ Googleの認証画面を表示できる
✔ Googleアカウントでユーザー登録できる
Time: 4.07 seconds, Memory: 18.00 MB
OK (2 tests, 3 assertions)
ブラウザでhttps://localhost/auth/google
に直接アクセスしてみてください。
Googleの認証画面が表示されると思います。
Googleからユーザーデータを受け取ってDBに保存する
まずテストを修正します
// 以下を追加
use App\User;
use Auth;
use Socialite;
use Mockery;
use Illuminate\Foundation\Testing\RefreshDatabase;
class OAuthTest extends TestCase
{
use RefreshDatabase;
public function setUp(): void
{
parent::setUp();
Mockery::getConfiguration()->allowMockingNonExistentMethods(false);
$this->providerName = 'google';
// モックを作成
$this->user = Mockery::mock('Laravel\Socialite\Two\User');
$this->user
->shouldReceive('getId')
->andReturn(uniqid())
->shouldReceive('getEmail')
->andReturn(uniqid().'@test.com')
->shouldReceive('getNickname')
->andReturn('Pseudo');
$this->provider = Mockery::mock('Laravel\Socialite\Contracts\Provider');
$this->provider->shouldReceive('user')->andReturn($this->user);
}
public static function tearDownAfterClass(): void
{
// Mockeryの設定をもとに戻す
Mockery::getConfiguration()->allowMockingNonExistentMethods(true);
}
/**
* 省略
*/
/**
* @test
*/
public function Googleアカウントでユーザー登録できる()
{
Socialite::shouldReceive('driver')->with($this->providerName)->andReturn($this->provider);
// URLをコール
$this->get(route('oauthCallback', ['service' => $this->providerName]))
->assertStatus(302)
->assertRedirect(route('home'));
// 各データが正しく登録されているかチェック
$this->assertDatabaseHas('users', [
'provider_id' => $this->user->getId(),
'provider_name' => $this->providerName,
'name' => $this->user->getNickName(),
'email' => $this->user->getEmail()
]);
// 認証チェック
$this->assertAuthenticated();
}
}
RefreshDataBase
RefreshDataBase
トレイトをuse
します。
これにより、テストクラス実行前にmigration
が走り、テスト終了後にrollback
されます。
Mockery::getConfiguration()->allowMockingNonExistentMethods(false)
Mockに対して、そのクラスに本来存在しないメソッドが追加された場合、エラーとするかどうかを設定できます。
デフォルトでtrue
、つまり存在しないメソッドが追加されることを許可する設定になっています。
ですが、ライブラリがメソッド名を変更した場合などにエラーになってほしいので、false
を設定します。
試しに以下のような処理を追加して実行するとエラーになるのがわかると思います。
$this->user->shouldReceive('foo')->andReturn(uniqid())
このままでは、他のテストクラスも影響を受けてしまいます。
デフォルトの挙動を期待して他のテストを書いていることもあると思います。
最後のテストメソッドが終了したあとに呼ばれるtearDownAfterClass()
でデフォルト設定に戻しましょう。
Mockery::mock('Laravel\Socialite\Two\User')
そのままです。
Socialite
のUser
クラスのモックを作成しています。
shouldReceive()->andReturn()
モックのメソッドの戻り値を設定しています。
$this->user->shouldReceive('getId')->andReturn(12345)
// 12345が返ってきます
$this->user->getId()
shouldReceive()->with()
with()
はshouldReceive()
で指定したメソッドの引数を設定できます。
以下の処理が各サービスからコールバックされた動きをモックしています。
Socialite::shouldReceive('driver')->with($this->providerName)->andReturn($this->provider);
以降の処理は、渡したデータが正しくDBに格納されているかの確認と、認証が正しく行われているかの確認になります。
この状態でPHPUnitを実行するとテストは失敗するはずです。
コントローラーを修正します
// 追加
use App\User;
use Auth;
class OAuthLoginController extends Controller
{
/**
* 省略
*/
/**
* 各サイトからのコールバック
* @param string $provider サービス名
* @return mixed
*/
public function handleProviderCallback($provider)
{
$socialUser = Socialite::driver($provider)->user();
$user = $this->user->firstOrNew(['email' => $socialUser->getEmail()]);
// すでに会員になっている場合の処理を書く
// そのままログインさせてもいいかもしれない
if ($user->exists) {
abort(403);
}
$user->name = $socialUser->getNickname();
$user->provider_id = $socialUser->getId();
$user->provider_name = $provider;
$user->save();
Auth::login($user);
return redirect()->route('home');
}
}
説明不要だとは思いますが一応
Googleから渡されたユーザーデータから、メールアドレスを取得します。
すでに登録されている場合は会員登録済みと判断します。
上記では403
にしていますが、そのままログインでも良いような気がします。
そのへんはお好みで
あとはUsers
テーブルに渡されたデータをINSERT
し、その情報で認証しているだけです。
テストを実行します
今度はすべてのテストをパスするはずです。
./vendor/bin/phpunit --testdox tests/Feature/OAuthTest.php
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.
Feature\OAuth
✔ Googleの認証画面を表示できる
✔ Googleアカウントでユーザー登録できる
Time: 5.05 seconds, Memory: 22.00 MB
OK (2 tests, 19 assertions)
ブラウザでhttps://localhost/auth/google
に直接アクセスし、Googleの認証画面で認証してください。
https://localhost/home
へリダイレクトされると思います。
以上です。
参考
https://readouble.com/laravel/5.8/ja/socialite.html
https://qiita.com/kite_999/items/bddd62c395f260e745bc
https://readouble.com/mockery/1.0/ja/index.html