LaravelでOAuth認証（GitHub）する方法

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

1. Application Name（Webアプリケーションの名前）
1. Homepage URL（ホームページのURL）
1. Authorization callback URL（GitHubで認証が完了した後の戻り先のURL）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397449/fc82e93d-9f86-907d-fca9-14262ced0355.png)

## 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テーブルははじめから存在してるマイグレーションファイルを今回は編集しました。

```php: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
```

```php:/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でもログインすることがあることを考慮してます。

```php: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);
    }
}
```

```php: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
<?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'); 
    }
}

```

# ルーティング設定
ルーティングを設定します。

``` 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のリンクを追加
```php: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のリンクを追加
``` php: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のボタンを押すとログイン成功画面が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397449/8c3a26e6-0db2-3573-efb9-b90c24cbeb03.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397449/5c845209-78e1-5c0b-0f68-bd5568718f46.png)