laravel

Laravel6でOAuth認証(GitHub)する方法

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項目を記入していきます。

  1. Application Name(Webアプリケーションの名前)
  2. Homepage URL(ホームページのURL)
  3. Authorization callback URL(GitHubで認証が完了した後の戻り先のURL)
Github register new oauth application -

Webアプリ側でOAuth認証の準備

GitHubへのアプリケーション登録後に作成されるClitent IDとClient Secretの値をconfig/service.phpに記述します。

//config/service.php
'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'redirect' => env('GITHUB_CALLBACK_URL'),
] 

.env関数から参照するようにしています。

こちらにはGitHubで取得した値を記述してください。

//.env
GITHUB_CLIENT_ID='Client ID'
GITHUB_CLIENT_SECRET='Client secrets'
GITHUB_CALLBACK_URL="${APP_URL}/social-auth/github/callback"

マイグレーションファイルの作成

user情報を登録するテーブルを作成していきます。

パスワードはnullableにすることでなくても登録できるようにしています。

userテーブルははじめから存在してるマイグレーションファイルを今回は編集しました。

//database / migrations / xxxxxx_create_users_table.php
<?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
// /database/migrations/xxxx_xx_xx_xxxxxx_create_social_accounts_table.php
<?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でもログインすることがあることを考慮してます。

// /app/User.php
<?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);
    }
}
// /app/SocialAccount.php
<?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'); 
    }
}

ルーティング設定

web.phpにルーティングを設定します。

// 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のリンクを追加

// resources / view / auth / login.blade.php
@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のリンクを追加

//resources / view / auth / register.blade.php
@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のボタンを押すとログイン成功画面が出力されます。

今回作成したコードはgithubにあげております。

https://github.com/kt5007/laravel-OAuth

参考文献

書籍

PHPフレームワークLaravel Webアプリケーション開発 バージョン8.x対応

記事

Laravel Socialite

OAuthの概要とそれを取り巻く環境

Laravel6ログイン機能を実装する

【Laravel6】SocialiteでGitHubアカウントを使用した認証システムの実装方法

Add Social Login in Laravel With Socialite