
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に表示されます。
SNS認証の追加
Composerを使ってSocialiteをインストールします。
composer require laravel/socialite
インストールが終われば通常のLaravelパッケージと同様に、Socialiteのサービスプロバイダーとファサードがconfig/app.phpに登録されます。
<?php
// ...
'providers' => [
// ...
/*
* Package Service Providers...
*/
Laravel\Socialite\SocialiteServiceProvider::class,
],
// ...
こちらがファサードのエイリアスです。
<?php
// ...
'aliases' => [
// ...
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
],
// ...
Socialiteは、サービスコンテナの内部にレイジーロードするシングルトンサービスとして登録されます。
設定
使いたいプロバイダーのプラットホームにOAuthアプリケーションを登録します。プロバイダーのAPIと通信するためのclient IDとclient 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テーブルのemailとpasswordフィールドをnullableにします。
Laravelのスキーマビルダーでスキーマを修正します。既存テーブルのフィールドを変更する前にdoctrine/dbalパッケージをインストールします。
composer require doctrine/dbal
最初はusersです。
php artisan make:migration prepare_users_table_for_social_authentication --table users
続いてemailとpasswordフィールドをnullableにします。
<?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
<?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アカウントと接続することもあるので、UserとLinkedSocialAccountsは1対多のリレーションにします。1対多のリレーションを実装するためにUser モデルに次のメソッドを追加します。
// ...
public function accounts(){
return $this->hasMany('App\LinkedSocialAccount');
}
// ...
逆のリレーションをLinkedSocialAccount modelに追加します。
<?php
// ...
public function user()
{
return $this->belongsTo('App\User');
}
// ...
続いてprovider_nameとprovider_idをLinkedSocialAccountsの$fillable配列に追加して、複数の値を保存できるようにします。
<?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'
コントローラークラスを次のように編集します。
<?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ネームスペースにファイルを作成し次のコードを記入します。
<?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本設定します。
<?php
// ...
Route::get('login/{provider}', 'Auth\SocialAccountController@redirectToProvider');
Route::get('login/{provider}/callback', 'Auth\SocialAccountController@handleProviderCallback');
この2本のルートを任意のプロバイダーで使えるように、ルートパラメーターproviderを使っています。
事例:Github経由の認証
これまでのコードをテストするために、SNS認証(ログイン)の選択肢としてGitHubを追加します。
最初はGitHubに新しいOAuthアプリケーションを登録します。
アプリ作成ページに必要な項目を入力します。
- Application Name:作成するアプリケーションを説明する名前を入力する。入力した名前がアプリケーションにログインしようとしてGithubにリダイレクトされたユーザーに表示される
- Homepage URL:作成するWebサイトのURLでhttp://localhost:8000、または有効なドメインを指定する
- Authorization Callback URL:作成するWebサイトのエンドポイントで、認証完了後にユーザーがリダイレクトされる
アプリケーションを作成するとエディットページにリダイレクトされるので、そこでClient IDとSecret keyを取得します。
設定
次にGitHubのClient IDとSecret keyを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ファイルに自動的に読み込みます。これによりコードを触らずに本番環境で値を変更できます。
GITHUB_CLIENT_ID=API Key
GITHUB_CLIENT_SECRET=API secret
GITHUB_URL=callbackurl
ログインページにGitHubへのリンクを追加
最後にGitHubへのリンクをログインページに追加します。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 -->
次のような画面になります。
「Login with Github」をクリックすると、Githubの認証ページが表示されます。
Socialite Providersプロジェクト
Socialite Providersはたくさんの非公式プロバイダーをSocialiteで使えるようにするプロジェクトで、コミュニティーで開発が進められています。プロバイダーは独立したパッケージとしてComposer経由でインストールされます。
プロバイダーはSocialite Providersプロジェクトで開発されたManager packageを使ってSocialite providersに登録します。このpackageはプロバイダーと同時に依存オブジェクトとしてインストールされます。
Manager packageはLaravelのサービスプロバイダーに含まれており、Socialiteデフォルトのサービスプロバイダーを継承しています。Socialite Providersコレクションのプロバイダーを使うときには、Socialiteのサービスプロバイダーを置き換えます。
// ...
SocialiteProviders\Manager\ServiceProvider:class,
// ...
注意:サービスプロバイダー(Service Providers)とSocialite Providersは、名前は似ているものの異なるものなので混同しないでください。サービスプロバイダーはLaravelのサービスコンテナにサービスを登録するクラスであり、Socialite Providers(または単にプロバイダー:Providers)はOAuthプロバイダーとやり取りするクラスです。
コレクションに含まれるプロバイダーにはそれぞれイベントリスナーがあり、app/Provider/EventServiceProviderクラスに追加して、SocialiteWasCalledイベントを検知できるようにします。
SocialiteにアクセスするとSocialiteWasCalledイベントが発動し、このイベントを待ち受けているプロバイダーはSocialiteに登録されます(オブザーバーパターンの実装)。
<?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_ID、CLIENT_SECRET、REDIRECT_URLの文頭にプロバイダー名を付けたものです。
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」のリンクを追加します。
<!-- Login page HTML code -->
<a href="/login/spotify" class="btn btn-default btn-md">Login with Spotify</a>
<!-- Login page HTML code -->
次のようなログインページが表示されます。
ルートは先ほどの事例で定義したものを再利用(Github経由の認証)するか、新しいコントローラーとロジックで作成します。
Login with SpotifyをクリックするとSpotifyの認証ページにリダイレクトされます。
この画面が表示されれば成功です!
カスタムプロバイダーの作成
Socialite Providersコレクションにプロバイダーがない場合には、自分で簡単に作成できます。
プロバイダーには次の2要素が必要です。
- providerクラス
- イベントリスナー
Providerクラス
providerクラスにはOAuthに関連するオペレーションのロジックすべてを実装します。
注:もしOAuth 1.0も使いたいなら、providerクラスを別に用意する必要があります。
最初にSocialite Providersコレクションに含まれているDeezerのproviderクラスを示します。
<?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のイベントリスナーを示します。
<?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]
