PHP

面倒なソーシャルログインをLaravelでサクッと実装!Socialiteが便利だ

2017/04/28

Reza Lavaryan

99

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

FacebookやTwitterなどのアカウントを使ったソーシャルログイン(SNSログイン)。個別に実装しようとすると結構手間ですが、Socialiteを使えば実装の手間を減らせますね。

Laravel Socialiteは複雑なSNS認証を高機能で使いやすいインターフェイスとして手軽に組み込めるパッケージです。

SocialiteがOAuthプロバイダーとしてサポートしているのはGoogle、Facebook、Twitter、LinkedIn、GitHub、Bitbucketです。サポート対象が拡大される予定はないものの、コミュニティーが開発を進めているコレクション、Socialite Providersを使えば非公式ですが多くのSNSをSocialiteのOAuthプロバイダーとして使えます。詳しくは、後で説明します。

この記事では、Laravelアプリケーションのインスタンスが稼働していて、コードを実際に試せる環境が整っていることを前提にしています。もし開発環境が必要なら、無料のHomestead改良版を使えます。

フォーム認証

OAuth認証に入る前に、Laravel標準のフォーム認証を設定します。Artisanコマンドmake:authを実行して、必要なビューと認証エンドポイントをインストールしてください。

php artisan make:auth

usersテーブルを作成するためにphp artisan migrateも実行します。

これでBootstrapスタイルのログインページが/loginに表示されます。

Laravel form based authentication

SNS認証の追加

Composerを使ってSocialiteをインストールします。

composer require laravel/socialite

インストールが終われば通常のLaravelパッケージと同様に、Socialiteのサービスプロバイダーとファサードがconfig/app.phpに登録されます。

File: config/app.php
<?php

// ...

'providers' => [

        // ...

        /*
         * Package Service Providers...
         */      
        Laravel\Socialite\SocialiteServiceProvider::class,

    ],

// ...

こちらがファサードのエイリアスです。

<?php

// ...
'aliases' => [

        // ...

        'Socialite' => Laravel\Socialite\Facades\Socialite::class,

    ],
// ...

Socialiteは、サービスコンテナの内部にレイジーロードするシングルトンサービスとして登録されます。

設定

使いたいプロバイダーのプラットホームにOAuthアプリケーションを登録します。プロバイダーのAPIと通信するためのclient IDclient secret keyを入手します。

プロバイダーごとにclient IDとsecret keyをconfig/services.phpに追加します。

// ...

'facebook' => [
        'client_id'     => env('FB_CLIENT_ID'),
        'client_secret' => env('FB_CLIENT_SECRET'),
        'redirect'      => env('FB_URL'),
],

'twitter' => [
        'client_id'     => env('TWITTER_CLIENT_ID'),
        'client_secret' => env('TWITTER_CLIENT_SECRET'),
        'redirect'      => env('TWITTER_URL'),
],

'github' => [
        'client_id'     => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect'      => env('GITHUB_URL'),
],

// ...

実際のkeyはプロジェクトルートディレクトリにある.envファイルに記入します。

データベースの検討

usersテーブルはSNS認証を組み込むことを想定してデザインされていないので、少し工夫が必要です。

SNS認証を選んだユーザーにはパスワード設定を通常は求めません(OAuth認証後にパスワードを要求するのは避けてください)。さらに選択したOAuthプロバイダーには登録メールアドレスがないかもしれません。したがって、usersテーブルのemailpasswordフィールドをnullableにします。

Laravelのスキーマビルダーでスキーマを修正します。既存テーブルのフィールドを変更する前にdoctrine/dbalパッケージをインストールします。

composer require doctrine/dbal

最初はusersです。

php artisan make:migration prepare_users_table_for_social_authentication --table users

続いてemailpasswordフィールドをnullableにします。

File: database/migrations/xxxxxx_prepare_users_table_for_social_authentication.php
<?php

// ...

/**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {

        // Making email and password nullable
            $table->string('email')->nullable()->change();
            $table->string('password')->nullable()->change();

        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {

            $table->string('email')->nullable(false)->change();
            $table->string('password')->nullable(false)->change();

        });
    }

// ...

ユーザーが選択したSNSアカウントへのリンクを保存するモデルとマイグレーションファイルを作成します。

php artisan make:model LinkedSocialAccount --migration
File: database/migrations/xxxxxx_create_linked_social_accounts_table.php
<?php

// ...

public function up()
    {
        Schema::create('linked_social_accounts', function (Blueprint $table) {

            $table->increments('id');
            $table->bigInteger('user_id');           
            $table->string('provider_name')->nullable();
            $table->string('provider_id')->unique()->nullable();          
            $table->timestamps();

        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('linked_social_accounts');
    }

// ...

provider_nameはプロバイダー名、provider_idはそのプロバイダーに登録されているユーザーのIDです。

migrateを実行して変更を適用します。

php artisan migrate

モデル

1人のユーザーが複数のSNSアカウントと接続することもあるので、UserLinkedSocialAccountsは1対多のリレーションにします。1対多のリレーションを実装するためにUser モデルに次のメソッドを追加します。

File: app/User.php
 // ...

public function accounts(){
    return $this->hasMany('App\LinkedSocialAccount');
}

 // ...

逆のリレーションをLinkedSocialAccount modelに追加します。

File: app/LinkedSocialAccounts.php
<?php
// ...

public function user()
{
    return $this->belongsTo('App\User');
}

// ...

続いてprovider_nameprovider_idLinkedSocialAccounts$fillable配列に追加して、複数の値を保存できるようにします。

File: app/LinkedSocialAccounts.php
<?php

// ...

protected $fillable = ['provider_name', 'provider_id' ];

public function user()
{
    return $this->belongsTo('App\User');
}

create()メソッドでユーザーとSNSアカウントを関連付けられるようになりました。

コントローラー

Authネームスペースにコントローラーを作成します。コントローラークラスにはOAuthプロバイダーへユーザーをリダイレクトするアクションとプロバイダーからコールバックを受け取るアクションが必要です。

php artisan make:controller 'Auth\SocialAccountController'

コントローラークラスを次のように編集します。

File: app/Http/Controllers/Auth/SocialAccountController.php
<?php

    /**
     * Redirect the user to the GitHub authentication page.
     *
     * @return Response
     */
    public function redirectToProvider($provider)
    {
        return \Socialite::driver($provider)->redirect();
    }

    /**
     * Obtain the user information
     *
     * @return Response
     */
    public function handleProviderCallback(\App\SocialAccountsService $accountService, $provider)
    {

        try {
            $user = \Socialite::with($provider)->user();
        } catch (\Exception $e) {
            return redirect('/login');
        }

        $authUser = $accountService->findOrCreate(
            $user,
            $provider
        );

        auth()->login($authUser, true);

        return redirect()->to('/home');
    }
}

上のコードではredirectToProvider()がプロバイダーのredirect()メソッドを呼び出して、ユーザーをSNS認証エンドポイントへリダイレクトしています。

<?php
// ...
 return Socialite::driver($provider)->redirect();
// ...

またredirect()を呼び出す前に、デフォルトのスコープをscopes()で変更できます。

<?php
// ...
 return Socialite::driver($provider)->scopes(['users:email'])->redirect();
// ...

OAuthプロバイダーは予期しない動作をすることもあるので、try/catchブロックを使います。例外が発生することなく進めばuserオブジェクト(Laravel\Socialite\Contracts\Userのインスタンス)をプロバイダーから受け取ります。userオブジェクトにはユーザー情報を取得するgetterメソッドがあり、ユーザーの名前、メールアドレス、アクセストークンなどを取得できます。使用可能なメソッドはドキュメントを参照してください。

次にローカルのuserオブジェクト(アプリのusersテーブルに格納されている)を取得するか、存在しなければ作成します。具体的にはヘルパークラスSocialAccountsService(このクラスは変数としてhandleProviderCallback()メソッドに渡します)からfindOrCreate()を呼び出します。

userオブジェクトを取得後にユーザーをログインさせ、ダッシュボードページへリダイレクトします。

ヘルパークラスSocialAccountService.phpを作成しましょう。

Appネームスペースにファイルを作成し次のコードを記入します。

File: app/SocialAccountService.php
<?php

namespace App;

use Laravel\Socialite\Contracts\User as ProviderUser;

class SocialAccountService
{
    public function findOrCreate(ProviderUser $providerUser, $provider)
    {
        $account = LinkedSocialAccount::where('provider_name', $provider)
                   ->where('provider_id', $providerUser->getId())
                   ->first();

        if ($account) {
            return $account->user;
        } else {

        $user = User::where('email', $providerUser->getEmail())->first();

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

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

        return $user;

        }
    }
}

このクラスの役割はローカルuserと関連するSNSアカウントを作成または取得することだけで、メソッドは1つだけです。

findOrCreateメソッドは現在のプロバイダーIDに関連したSNSアカウントが登録されているか確認するクエリをlinked_social_accountsテーブルに発行し、登録されていて、SNSアカウントを含むローカルuserオブジェクトを返します。

<?php
// ...

if ($account) {
   return $account->user;
}

// ...

userが存在しないか接続前なら、SNSアカウントは見つかりません。ユーザーが登録フォームから登録している可能性があるので、メールでusersテーブルを検索します。それでもユーザーが見つからなければ、新たにuserのエントリーを作成し現在のSNSを関連付けます。

ルート

SNS認証のためにルートを2本設定します。

File: routes/web.php
<?php

// ...

Route::get('login/{provider}',          'Auth\SocialAccountController@redirectToProvider');
Route::get('login/{provider}/callback', 'Auth\SocialAccountController@handleProviderCallback');

この2本のルートを任意のプロバイダーで使えるように、ルートパラメーターproviderを使っています。

事例:Github経由の認証

これまでのコードをテストするために、SNS認証(ログイン)の選択肢としてGitHubを追加します。

最初はGitHubに新しいOAuthアプリケーションを登録します。

Github app creation page

アプリ作成ページに必要な項目を入力します。

  • Application Name:作成するアプリケーションを説明する名前を入力する。入力した名前がアプリケーションにログインしようとしてGithubにリダイレクトされたユーザーに表示される
  • Homepage URL:作成するWebサイトのURLでhttp://localhost:8000、または有効なドメインを指定する
  • Authorization Callback URL:作成するWebサイトのエンドポイントで、認証完了後にユーザーがリダイレクトされる

アプリケーションを作成するとエディットページにリダイレクトされるので、そこでClient IDとSecret keyを取得します。

Github app edit page

設定

次にGitHubのClient IDとSecret keyをconfig/services.phpに追加します。

File: config/services.php
<?php

// ...

'github' => [
        'client_id'     => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect'      => env('GITHUB_URL'),
],

// ...

Client ID、Secret key、コールバックURLを直接config/services.phpに書き込むのではなく、アプリケーションの.envファイルに保存して、getenv()services.phpファイルに自動的に読み込みます。これによりコードを触らずに本番環境で値を変更できます。

File: .env
GITHUB_CLIENT_ID=API Key
GITHUB_CLIENT_SECRET=API secret
GITHUB_URL=callbackurl

ログインページにGitHubへのリンクを追加

最後にGitHubへのリンクをログインページに追加します。resources/views/auth/login.blade.phpを開いて、次のコードを適切な位置に追加します。

File: resources/views/auth/login.blade.php
<!-- Login page HTML code  -->

<a href="/login/github"  class="btn btn-default btn-md">Log in with Github</a>

<!-- Login page HTML code  -->

次のような画面になります。

Log in form with Github link

「Login with Github」をクリックすると、Githubの認証ページが表示されます。

Github authorization endpoint

Socialite Providersプロジェクト

Socialite Providersはたくさんの非公式プロバイダーをSocialiteで使えるようにするプロジェクトで、コミュニティーで開発が進められています。プロバイダーは独立したパッケージとしてComposer経由でインストールされます。

プロバイダーはSocialite Providersプロジェクトで開発されたManager packageを使ってSocialite providersに登録します。このpackageはプロバイダーと同時に依存オブジェクトとしてインストールされます。

Manager packageはLaravelのサービスプロバイダーに含まれており、Socialiteデフォルトのサービスプロバイダーを継承しています。Socialite Providersコレクションのプロバイダーを使うときには、Socialiteのサービスプロバイダーを置き換えます。

File: config/app.php
        // ...

        SocialiteProviders\Manager\ServiceProvider:class,

        // ...

注意:サービスプロバイダー(Service Providers)とSocialite Providersは、名前は似ているものの異なるものなので混同しないでください。サービスプロバイダーはLaravelのサービスコンテナにサービスを登録するクラスであり、Socialite Providers(または単にプロバイダー:Providers)はOAuthプロバイダーとやり取りするクラスです。

コレクションに含まれるプロバイダーにはそれぞれイベントリスナーがあり、app/Provider/EventServiceProviderクラスに追加して、SocialiteWasCalledイベントを検知できるようにします。

SocialiteにアクセスするとSocialiteWasCalledイベントが発動し、このイベントを待ち受けているプロバイダーはSocialiteに登録されます(オブザーバーパターンの実装)。

File: app/Providers/EventServiceProvider.php
<?php

// ...

protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        'SocialiteProviders\Deezer\DeezerExtendSocialite@handle',
    ],
];

上の例では、プロバイダーをDeezer経由で認証できるように登録しています。

注意:Socialiteの標準プロバイダーは同名のプロバイダーで、オーバーライドしない限り引き続き使用できます。

事例:Spotify経由の認証

Socialite Providersの例としてSpotifyをログインの選択肢に追加します。

最初にSocialite Providersにアクセスして、Spotifyのプロバイダーを左サイドバーの中から見つけます。

インストールと使い方を説明したマニュアルがプロバイダーごとに用意されています。Composerを使ってSpotifyのプロバイダーをインストールします。

composer install socialproviders/spotify

設定

先ほどと同様にアプリをSpotifyの開発者プラットホームに登録し、Client IDとSecret keyを取得して、アプリに設定します。

Manager packageを使えば、新しいプロバイダーの設定は簡単です。標準のプロバイダーとは違って、config/services.phpにプロバイダーごとに行を追加する必要はありません。代わりにアプリケーションの.envファイルに設定を追加するだけです。Manager packageのConfig Retrieverヘルパークラスのおかげでできることです。

設定はCLIENT_IDCLIENT_SECRETREDIRECT_URLの文頭にプロバイダー名を付けたものです。

File: .env
SPOTIFY_CLIENT_ID = YOUR_CLIENT_ID_ON_SPOTIFY
SPOTIFY_CLIENT_SECRET = YOUR_CLIENT_SECRET_ON_SPOTIFY
SPOTIFY_REDIRECT_URL = YOUR_CALL_BACK_URL

ビュー

次にログインページに「Login with Spotify」のリンクを追加します。

File: resources/views/auth/login.blade.php
<!-- Login page HTML code  -->

<a href="/login/spotify"  class="btn btn-default btn-md">Login with Spotify</a>

<!-- Login page HTML code  -->

次のようなログインページが表示されます。

Login page with Spotify link

ルートは先ほどの事例で定義したものを再利用(Github経由の認証)するか、新しいコントローラーとロジックで作成します。

Login with SpotifyをクリックするとSpotifyの認証ページにリダイレクトされます。

Spotify authorization endpoint

この画面が表示されれば成功です!

カスタムプロバイダーの作成

Socialite Providersコレクションにプロバイダーがない場合には、自分で簡単に作成できます。

プロバイダーには次の2要素が必要です。

  • providerクラス
  • イベントリスナー

Providerクラス

providerクラスにはOAuthに関連するオペレーションのロジックすべてを実装します。

:もしOAuth 1.0も使いたいなら、providerクラスを別に用意する必要があります。

最初にSocialite Providersコレクションに含まれているDeezerのproviderクラスを示します。

File: vendor/socialiteproviders/deezer/Provider.php
<?php

namespace SocialiteProviders\Deezer;

use SocialiteProviders\Manager\OAuth2\User;
use Laravel\Socialite\Two\ProviderInterface;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;

class Provider extends AbstractProvider implements ProviderInterface
{
    /**
     * Unique Provider Identifier.
     */
    const IDENTIFIER = 'DEEZER';

    /**
     * {@inheritdoc}
     */
    protected $scopes = ['basic_access', 'email'];

    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase(
            'https://connect.deezer.com/oauth/auth.php', $state
        );
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return 'https://connect.deezer.com/oauth/access_token.php';
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken($token)
    {
        $response = $this->getHttpClient()->get(
            'https://api.deezer.com/user/me?access_token='.$token
        );

        return json_decode($response->getBody()->getContents(), true);
    }

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject(array $user)
    {
        return (new User())->setRaw($user)->map([
            'id' => $user['id'], 'nickname' => $user['name'],
            'name' => $user['firstname'].' '.$user['lastname'],
            'email' => $user['email'], 'avatar' => $user['picture'],
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function getCodeFields($state = null)
    {
        return [
            'app_id' => $this->clientId, 'redirect_uri' => $this->redirectUrl,
            'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
            'state' => $state, 'response_type' => 'code',
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getAccessToken($code)
    {
        $url = $this->getTokenUrl().'?'.http_build_query(
            $this->getTokenFields($code), '', '&', $this->encodingType
        );

        $response = file_get_contents($url);

        $this->credentialsResponseBody = json_decode($response->getBody(), true);

        return $this->parseAccessToken($response->getBody());
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenFields($code)
    {
        return [
            'app_id' => $this->clientId,
            'secret' => $this->clientSecret,
            'code' => $code,
        ];
    }

    /**
     * {@inheritdoc}
     */
    protected function parseAccessToken($body)
    {
        parse_str($body, $result);

        return $result['access_token'];
    }
}

providerクラスは抽象クラスLaravel\Socialite\Two\AbstractProviderを継承しています。この抽象クラスにはOAuth 2.0のオペレーション全般を扱うメソッドがあり、スコープのフォーマットやアクセストークンの取得と使用などができます。この抽象クラスを継承して抽象メソッドを実装します。

加えてProviderInterfaceも実装します。このインターフェイスによりredirect()user()メソッドの実装が必要になります。

すでに述べたとおり、redirect()はユーザーをOAuthプロバイダーの認証ページへリダイレクトし、user()Laravel\Socialite\Contracts\Userのインスタンスを返します。このインスタンスにはプロバイダーが保有するユーザーの情報が含まれます。

プロバイダーのイベントリスナー

プロバイダーのイベントリスナーは、SocialiteWasCalledイベント発動時にプロバイダーをSocialite providerとして登録するクラスです。

Deezerのイベントリスナーを示します。

File: vendor/socialiteproviders/deezer/DeezerExtendSocialite.php
<?php

namespace SocialiteProviders\Deezer;

use SocialiteProviders\Manager\SocialiteWasCalled;

class DeezerExtendSocialite
{
    /**
     * Register the provider.
     *
     * @param \SocialiteProviders\Manager\SocialiteWasCalled $socialiteWasCalled
     */
    public function handle(SocialiteWasCalled $socialiteWasCalled)
    {
        $socialiteWasCalled->extendSocialite(
            'deezer', __NAMESPACE__.'\Provider'
        );
    }
}

SocialiteWasCalledイベントにはextendSocialite()メソッドがあり、プロバイダーのクラスを引数として受け取りSocialiteに登録します。

最後に

SNS認証はLaravelを使えば簡単に実装できます。記事ではさまざまなOAuthプロバイダーを使ってユーザーを認証する方法やカスタムプロバイダーを作る方法を説明しました。

プロバイダーの名前とIDに加えて、アバター、アクセストークン、リフレッシュトークン(該当時)などのSNSの情報をusersテーブルに保存できます。プロバイダーのAPIで通信もでき、なんらかの操作をユーザーの代理で実行もできます。もちろんユーザーの許可を得ている場合だけですが。

この記事のコード全文はGitHubで入手できます。実際に自分で試してみてください。

※本記事はWern Anchetaが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Easily Add Social Logins to Your App with Socialite

[翻訳:内藤夏樹/編集:Livit
[Image:mirtmirt / Shutterstock.com]

Copyright © 2017, Reza Lavaryan All Rights Reserved.

Reza Lavaryan

Reza Lavaryan

イラン在住のWeb開発者でスタートアップにも熱中しています。フロントエンドとバックエンドの開発経験は10年以上あって、美しくシンプルなことを日々のモットーにしています。誰もが毎日なにか新しいものを学ぶべきと信じています。

Loading...