🐘

Laravel github認証(token)

2022/01/11に公開

環境

  • Laravel 8.77.1
    • socialite
  • PHP 8.1.1
  • MySQL
  • Next.js 12.0.7

バックエンド:https://localhost
フロントエンド:http://localhost:3000

docker

https://github.com/bz0/times-docker

懸念

cookieのapi_tokenはhttponlyにできない(JSで読み込めなくなりAPIを投げるときにセットできない)のでセキュリティ上の懸念はあります。


下記最低限の実装です。

バックエンド

socialiteのインストールを行う

$ composer require laravel/socialite

$guardsのapiのdriverをtokenにする

app/config/auth.php

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => true,
        ],
    ],

ログイン失敗時どこにリダイレクトするかを修正

app/app/Http/Middleware/Authenticate.php

    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return 'http://localhost:3000';
        }
    }

ルーティング設定

  • /login/{provider}(githubの認証ログイン)
  • /auth/{provider}/callback(githubからのコールバック)
  • /me(ログインユーザの取得)

app/routes/api.php

//認証成功すればユーザ情報取得 失敗すれば401が返る
Route::middleware('auth:api')->get('/me', function (Request $request) {
    return $request->user();
});

Route::get('/login/{provider}', [OAuthController::class, 'getProviderOAuthURL'])
            ->where('provider', 'github')->name('oauth.request');

Route::get('/auth/{provider}/callback', [OAuthController::class, 'handleProviderCallback'])
            ->where('provider', 'github')->name('oauth.callback');

app/app/Http/Controllers/Auth/OAuthController.php

<?php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Laravel\Socialite\Facades\Socialite;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use App\Enums\Provider;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cookie;

class OAuthController extends Controller
{
    /**
     * (各認証プロバイダーの)OAuth認可画面URL取得API
     * @param string $provider 認証プロバイダーとなるサービス名
     * @return \Illuminate\Http\JsonResponse
     */
    public function getProviderOAuthURL(string $provider)
    {
        $redirectUrl = Socialite::driver($provider)
                            ->scopes(['read:user', 'public_repo'])
                            ->redirect()
                            ->getTargetUrl();
        
        return response()->json([
            'redirect_url' => $redirectUrl,
        ]);
    }

    public static function generateToken()
    {
        return Str::random(80);
    }

     /**
     * ソーシャルログイン処理
     * @return App\User
     */
    public static function handleProviderCallback()
    {
        $githubUser = Socialite::driver('github')->stateless()->user();

        $user  = User::where('github_id', $githubUser->id)->first();
        $token = self::generateToken();
        
        if ($user) {
            $user->update([
                'name' => $githubUser->name,
                'email' => $githubUser->email,
                'bio' => $githubUser->user['bio'],
                'avatar_url' => $githubUser->user['avatar_url'],
                'github_token' => $githubUser->token,
                'github_refresh_token' => $githubUser->refreshToken,
                'api_token' => hash('sha256', $token)
            ]);
        } else {
            $user = User::create([
                'name' => $githubUser->name,
                'email' => $githubUser->email,
                'provider' => Provider::GITHUB,
                'bio' => $githubUser->user['bio'],
                'avatar_url' => $githubUser->user['avatar_url'],
                'github_id' => $githubUser->id,
                'github_token' => $githubUser->token,
                'github_refresh_token' => $githubUser->refreshToken,
                'api_token' => hash('sha256', $token)
            ]);
        }
    
        Auth::login($user);
    
        $cookie = cookie('api_token', $token, '10000000', null, null, null, false);
        return redirect('http://localhost:3000')->cookie($cookie);
    }
}

app/database/migrations/2014_10_12_000000_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->id();
            $table->string('name')->nullable();
            $table->string('email');

            //todo:github以外のログイン認証も追加予定

            $table->enum('provider', ['github']);
            $table->string('github_id');
            $table->string('avatar_url')->nullable();
            $table->text('bio')->nullable();
            $table->text('github_token');
            $table->text('github_refresh_token')->nullable();
            $table->rememberToken();
            $table->string('api_token', 80)
                ->unique()
                ->nullable()
                ->default(null);

            $table->timestamp('created_at')->default(\DB::raw('CURRENT_TIMESTAMP'));
            $table->timestamp('updated_at')->default(\DB::raw('CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP'));
        });
    }

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

migrateを実行(テーブル生成)

$ php artisan migrate

フロント

src/pages/index.tsx

import type { NextPage } from 'next'
import axios from 'axios'

const handleSocialLoginRequest = async (provider: any) => {
  const { data } = await axios.get(`https://localhost/api/login/${provider}`);
  console.log("data:",data.redirect_url);

  window.location.href = data.redirect_url;
}


const Home: NextPage = () => {
  return (
    <>
      <button type="submit" onClick={() => {handleSocialLoginRequest('github');}}>github login</button> 
    </>

  )
}

export default Home

src/pages/me.tsx

import type { NextPage } from 'next'
import axios from 'axios'
import { parseCookies, setCookie, destroyCookie } from 'nookies'

const handleSocialLoginRequest = async () => {
  const cookies = parseCookies()

  const { data } = await axios.get('https://localhost/api/me', {
    headers: {
      'Authorization' : 'Bearer ' + cookies['api_token']
    },
  });
  console.log("data:",data);
  return data;
}

const Me: NextPage = () => {
  const data = handleSocialLoginRequest();
  return (
    <>
      <div>me!!</div>
    </>
  )
}

export default Me

Discussion