69
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Laravel+Socialiteで簡単ソーシャルログイン実装! (テスト付き)

Last updated at Posted at 2019-04-20

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が対応しているサービスは以下

  • Google
  • Facebook
  • Twitter
  • Linkedin
  • GitHub
  • GitLab
  • Bitbucket

それぞれのサービスに登録してクライアントID、シークレットを取得してください。
今回はGoogleを利用します。

Google API Consoleにアクセスします。
https://console.developers.google.com/project
プロジェクトを作成をクリックしてプロジェクトを作成します。
プロジェクトのダッシュボードへ移動します。
左上のハンバーガーアイコンをクリック
APIとサービス > 認証情報 > 認証情報を作成 > OAuth クライアント IDでクライアントIDを作成します。
同意画面の設定を入力します。※ドメインやプライバシーポリシーのURLが必要です。(ドメインでホストされているURL)
前の画面へ戻り、アプリケーションの種類 > ウェブアプリケーションを選択
承認済みのリダイレクトURIを入力して保存
クライアントIDとシークレットが発行されます。(認証情報画面でいつでも確認可)

アプリケーションにクライアントIDとシークレットを設定する

各ファイルに以下を追加

.env
# 末尾でOK
GOOGLE_CLIENT_ID=上記で取得したID
GOOGLE_CLIENT_SECRET=上記で取得したシークレット
GOOGLE_CALLBACK_URL=承認済みのリダイレクトURIを入れてください
config/services.php
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ファイルを編集する

database/migrations/XXX_create_users_tabel.php
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を設定しても構いません。

config/database.php
return [
    /**
     * 省略
     */
    'connections' => [ 
        /**
         * 省略
         */

        // 追加
        'testing' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ],
    ],

    /**
     * 以下省略
     */
];
phpunit.xml
<?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

作成したコントローラーをひとまず以下のように編集します。

app/Http/Controllers/Auth/OAuthController.php
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 あとで実装
    }
}

ルーティングを設定する

routes/web.php
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のコンソールで入力する承認済みのリダイレクトURIauth/{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
tests/Feature/OAuthTest.php
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の認証画面を表示させる

まずテストを修正します

tests/Feature/OAuthTest.php
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の引数にはサービス名を渡します。

app/Http/Controllers/Auth/OAuthController.php
// 追加
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に保存する

まずテストを修正します

tests/Feature/OAuthTest.php
// 以下を追加
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')

そのままです。
SocialiteUserクラスのモックを作成しています。

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を実行するとテストは失敗するはずです。

コントローラーを修正します

app/Http/Controllers/Auth/OAuthController.php
// 追加
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

69
62
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?