この記事はAll About Group(株式会社オールアバウト) Advent Calendar 2022 8日目の記事です。
社内にタスク管理アプリがあり、ログイン機能を追加できたらと考えておりました。
そこで会社のGithubアカウントやGoogleアカウントを紐付けてユーザー管理できないかと考えてOAuth認証ができる方法を調べてみました。
OAuth認証とは
TwitterやFacebook、Googleなどの外部サービスのアカウントをログイン認証に使い、アプリケーション側ではユーザー情報だけを管理する仕組みをOAuth認証と言います。
Socialite
Socialiteとは
LaravelでSocialiteパッケージを使うことでOAuth認証を実装することができます。
Facebook、Twitter、Google、LinkedIn、GitLab、 Bitbucket等に対応しています。
提供していないサービスであっても独自のOAuth認証ドライバを用意することで利用が可能です。
Socialiteのインストール
Socialiteパッケージをインストールするにはアプリケーションのルートディレクトリ(composer.jsonが設置されているディレクトリ)で下記のコマンドを実行します。
composer require laravel/socialite
GitHub OAuth認証をWebアプリで使用するための設定
GitHub上でOAuth認証の準備
GitHub上でOAuthアプリケーションの作成を行います。
GitHubにログインして、[Settings]→[Developer Settings]→[OAuth Apps]→[New OAuth App]と画面遷移します。
以下の3項目を記入していきます。
- Application Name(Webアプリケーションの名前)
- Homepage URL(ホームページのURL)
- Authorization callback URL(GitHubで認証が完了した後の戻り先のURL)
Webアプリ側でOAuth認証の準備
GitHubへのアプリケーション登録後に作成されるClitent IDとClient Secretの値をconfig/service.phpに記述します。
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_CALLBACK_URL'),
]
.env関数から参照するようにしています。
こちらにはGitHubで取得した値を記述してください。
GITHUB_CLIENT_ID='Client ID'
GITHUB_CLIENT_SECRET='Client secrets'
GITHUB_CALLBACK_URL="${APP_URL}/social-auth/github/callback"
マイグレーションファイルの作成
user情報を登録するテーブルを作成していきます。
パスワードはnullableにすることで、存在しなくても登録できるようにしています。
userテーブルははじめから存在してるマイグレーションファイルを今回は編集しました。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password')->nullable();
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
またどのソーシャルアカウントを使っているかを管理するためのSocialAccountsテーブルを作成します。
php artisan make:migration create_social_accounts_table
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSocialAccountsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('social_accounts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->string('provider_name')->nullable();
$table->string('provider_id')->unique()->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('social_accounts');
}
}
編集しましたら下記コマンドを実行してください。
php artisan migrate
モデルとコントローラーの作成
モデルの作成
下記のコマンドからコントローラーとモデルを作成します。
php artisan make:controller Auth/SocialLoginController
php artisan make:model SocialAccount
UserモデルとSocialAccountモデルを編集します。
SocialAccountモデルと紐付けます。
SocialAccountは一人のユーザーに対して複数あるとしています。
例えばFacebookやGoogleでもログインすることがあることを考慮してます。
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function socialAccounts(){
return $this->hasMany(socialAccount::class);
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class SocialAccount extends Model
{
protected $fillable = [
'user_id', 'provider_name', 'provider_id'
];
// User
public function user()
{
return $this->belongsTo(User::class);
}
}
コントローラーの編集
OAuthで利用するURLは外部サービスの認証ページにリダイレクトするURLと外部サービスからコールバックされるURLの2つです。
コールバック時にSocialiteのメソッドを介してユーザー情報を取得します。
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Laravel\Socialite\Facades\Socialite;
use App\SocialAccount;
use App\User;
use Exception;
class SocialLoginController extends Controller
{
// GitHubの認証ページヘユーザーを転送するためのルート
public function redirectToProvider(String $provider)
{
return Socialite::driver($provider)->redirect();
}
// GitHubの認証後に戻るルート
public function providerCallback(String $provider)
{
// エラーならwelcome pageに遷移
try {
$social_user = Socialite::with($provider)->user();
} catch (Exception $e) {
return redirect('/welcome');
}
// nameかnickNameをuserNameにする
if ($social_user->getName()) {
$user_name = $social_user->getName();
} else {
$user_name = $social_user->getNickName();
}
// userテーブルに保存
$auth_user = User::firstOrCreate([
'email' => $social_user->getEmail(),
'name' => $user_name
]);
// social accountテーブルに保存
$auth_user->socialAccounts()->firstOrCreate([
'provider_id'=>$social_user->getId(),
'provider_name'=>$provider
]);
// ログイン
auth()->login($auth_user);
// homeページに転送
return redirect()->to('/home');
}
}
ルーティング設定
ルーティングを設定します。
// GitHubの認証後に戻るためのルーティング
Route::get('social-auth/{provider}/callback','Auth\SocialLoginController@providerCallback');
// GitHubの認証ベージに遷移するためのルーティング
Route::get('social-auth/{provider}','Auth\SocialLoginController@redirectToProvider')->name('social.redirect');
ユーザ登録からログイン認証画面の作成
Laravel/uiライブラリのインストール
laravel/uiライブラリをインストールします。
下記コマンドを実行してください。
composer require laravel/ui 1.*
php artisan ui vue --auth
npm install
npm run dev
ログイン画面にGitHubのリンクを追加
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('login') }}">
@csrf
{{-- githubへのリンク追加 --}}
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">Login With</label>
<div class="col-md-6">
<a href="{{ route('social.redirect', 'github') }}" class="btn btn-success">Github</a>
</div>
</div>
{{-- ここまで追加 --}}
<div class="form-group row">
<label for="email"
class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror"
name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password"
class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password"
class="form-control @error('password') is-invalid @enderror" name="password"
required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember"
{{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">
{{ __('Remember Me') }}
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Login') }}
</button>
@if (Route::has('password.request'))
<a class="btn btn-link" href="{{ route('password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
登録画面にGitHubのリンクを追加
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('register') }}">
@csrf
{{-- githubへのリンク追加 --}}
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">Login With</label>
<div class="col-md-6">
<a href="{{ route('social.redirect', 'github') }}" class="btn btn-success">Github</a>
</div>
</div>
{{-- ここまで追加 --}}
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror"
name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="email"
class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror"
name="email" value="{{ old('email') }}" required autocomplete="email">
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password"
class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password"
class="form-control @error('password') is-invalid @enderror" name="password"
required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password-confirm"
class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control"
name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
完成
Githubのボタンを押すとログイン成功画面が出力されます。