laravel ✕ OAuth2.0 Socialite laravel側DBでユーザーの情報を持たずにソーシャルログインを実装してみる

## 概要

今まで下記の記事て実装してきたlaravelのソーシャルアカウント（Googleアカウント）を用いたログインではソーシャルアカウントのユーザー名、メールアドレスをOIDCのUserinfoのAPIで取得し、ダミーパスワード文字列を発行し、それらをlaravel側のDBのusersテーブルに格納し、`Auth::login()`をしていた。
この方法でも問題なないのだが、今回はあえてユーザー名、メールアドレスをlaravel側のDBで保持しないで`Auth::login()`できるようにしてみる。

## 前提

「これまで」の実装は下記の記事の内容の直後という意味合い

https://qiita.com/miriwo/items/a7f90054d123855256f1

本プロジェクトのマイグレーションはフレッシュマイグレーションに対応しているものとする。

### これまでのテーブル定義

#### users

※説明に必要なカラムのみ抜粋

| カラム名| データ型| 属性 | 説明 |
| --- | --- | --- | --- |
| id | bigint unsigned | PRIMARY KEY, AUTO_INCREMENT | 主キー |
| name | varchar(255) | NOT NULL | ユーザー名 |
| email | varchar(255) | NOT NULL, UNIQUE | メールアドレス |
| password | varchar(255) | NOT NULL | ハッシュ化されたパスワード |
| google_id | varchar(255) | NULLABLE | Google OAuth ID |
| created_at | timestamp | NULLABLE | 作成日時 |
| updated_at | timestamp | NULLABLE | 更新日時 |

#### oauth_tokens

| カラム名 | データ型 | 属性 | 説明 |
| --- | --- | --- | --- |
| id| bigint unsigned | PRIMARY KEY, AUTO_INCREMENT | 主キー |
| user_id| bigint unsigned | NOT NULL, FOREIGN KEY | ユーザーID（users.id参照、CASCADE削除） |
| access_token | varchar(255) | NOT NULL | OAuthアクセストークン |
| refresh_token | varchar(255) | NULLABLE | OAuthリフレッシュトークン |
| access_token_expires_at | datetime | NOT NULL | アクセストークンの有効期限 |
| refresh_token_expires_at | datetime | NULLABLE | リフレッシュトークンの有効期限 |
| created_at | timestamp | NULLABLE | 作成日時 |
| updated_at | timestamp | NULLABLE | 更新日時 |

### 今回の対応後のテーブル定義

#### users

※説明に必要なカラムのみ抜粋

今回の修正を行うとusersテーブルにレコードは作成されなくなる。

| カラム名| データ型| 属性 | 説明 |
| --- | --- | --- | --- |
| id | bigint unsigned | PRIMARY KEY, AUTO_INCREMENT | 主キー |
| name | varchar(255) | NOT NULL | ユーザー名 |
| email | varchar(255) | NOT NULL, UNIQUE | メールアドレス |
| password | varchar(255) | NOT NULL | ハッシュ化されたパスワード |
| created_at | timestamp | NULLABLE | 作成日時 |
| updated_at | timestamp | NULLABLE | 更新日時 |

#### oauth_tokens

| カラム名 | データ型 | 属性 | 説明 |
| --- | --- | --- | --- |
| id| bigint unsigned | PRIMARY KEY, AUTO_INCREMENT | 主キー |
| google_id | varchar(255) | NULLABLE | Google OAuth ID |
| access_token | varchar(255) | NOT NULL | OAuthアクセストークン |
| refresh_token | varchar(255) | NULLABLE | OAuthリフレッシュトークン |
| access_token_expires_at | datetime | NOT NULL | アクセストークンの有効期限 |
| refresh_token_expires_at | datetime | NULLABLE | リフレッシュトークンの有効期限 |
| created_at | timestamp | NULLABLE | 作成日時 |
| updated_at | timestamp | NULLABLE | 更新日時 |

## 内容

1. usersテーブルにgoogle_idを追加したマイグレーションファイル`database/migrations/YYYY_MM_DD_XXXXXX_add_google_id_to_users_table.php`を削除
1. oauth_tokensテーブルのマイグレーションファイル`database/migrations/YYYY_MM_DD_XXXXXX_create_oauth_tokens_table.php`を下記の様に修正し、user_idカラムの削除とgoogle_idカラムの追加を実施

    ```database/migrations/YYYY_MM_DD_XXXXXX_create_oauth_tokens_table.php
    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        /**
         * Run the migrations.
         */
        public function up(): void
        {
            Schema::create('oauth_tokens', function (Blueprint $table) {
                // カラム定義
                $table->id();
                $table->string('google_id');
                $table->string('access_token');
                $table->string('refresh_token')->nullable();
                $table->datetime('access_token_expires_at');
                $table->datetime('refresh_token_expires_at')->nullable();
                $table->timestamps();
            });
        }
    
        /**
         * Reverse the migrations.
         */
        public function down(): void
        {
            Schema::dropIfExists('oauth_tokens');
        }
    };
    ```

1. OauthTokenのモデルを修正しテーブル定義にあわせる

    ```app/Models/OauthToken.php
    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Factories\HasFactory;
    use Illuminate\Database\Eloquent\Model;
    
    class OauthToken extends Model
    {
        /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
        use HasFactory;
    
        protected $fillable = [
            'google_id',
            'access_token',
            'refresh_token',
            'access_token_expires_at',
            'refresh_token_expires_at',
        ];
    }
    ```

1. フレッシュマイグレーション実行（筆者はsailを使っているのでsailコマンドを使用）

    ```
    sail artisan migrate:fresh
    ```

1. 下記の様なセッションユーザー用のクラスを新規で定義

    ```app/Auth/SessionUser.php
    <?php
    
    namespace App\Auth;
    
    use Illuminate\Contracts\Auth\Authenticatable;
    
    class SessionUser implements Authenticatable
    {
        protected $googleId;
    
        public function __construct(string $googleId)
        {
            $this->googleId = $googleId;
        }
    
        // 認証IDとして使う（Auth::id()で取得される）
        public function getAuthIdentifierName()
        {
            return 'google_id';
        }
    
        public function getAuthIdentifier()
        {
            return $this->googleId;
        }
    
        public function getAuthPassword()
        {
            return ''; // パスワード認証しない
        }
    
        public function getAuthPasswordName()
        {
            return 'password';
        }
    
        public function getRememberToken()
        {
            return null; // remember me 不使用
        }
    
        public function setRememberToken($value)
        {
            // remember me 不使用
        }
    
        public function getRememberTokenName()
        {
            return null;
        }
    
        // 任意：名前などをセッションから取得（ビュー用）
        public function getName()
        {
            return session('user_name');
        }
    
        public function getAvatar()
        {
            return session('avatar');
        }
    
        // プロパティアクセス用のマジックメソッド
        public function __get($name)
        {
            switch ($name) {
                case 'name':
                    return $this->getName();
                case 'email':
                    return session('user_email');
                case 'avatar':
                    return $this->getAvatar();
                case 'profile_photo_url':
                    return $this->getAvatar();
                default:
                    return null;
            }
        }
    }
    ```

1. セッションを用いた認証をlaravelの認証システムで使えるようにプロバイダーを作成

    ```app/Auth/SessionUserProvider.php
    <?php
    
    namespace App\Auth;
    
    use Illuminate\Contracts\Auth\UserProvider;
    use Illuminate\Contracts\Auth\Authenticatable;
    
    class SessionUserProvider implements UserProvider
    {
        /**
         * 識別子でユーザーを取得
         */
        public function retrieveById($identifier)
        {
            // セッションにgoogle_idが存在し、一致する場合はSessionUserを返す
            if (session()->has('google_id') && session('google_id') == $identifier) {
                return new SessionUser($identifier);
            }
            
            return null;
        }
    
        /**
         * トークンでユーザーを取得（remember me機能）
         */
        public function retrieveByToken($identifier, $token)
        {
            // 今回は不要（remember me機能）
            return null;
        }
    
        /**
         * remember tokenを更新
         */
        public function updateRememberToken(Authenticatable $user, $token)
        {
            // 今回は不要（remember me機能）
        }
    
        /**
         * 認証情報でユーザーを取得
         */
        public function retrieveByCredentials(array $credentials)
        {
            // 今回は不要（パスワード認証）
            return null;
        }
    
        /**
         * ユーザーの認証情報を検証
         */
        public function validateCredentials(Authenticatable $user, array $credentials)
        {
            // 今回は不要（パスワード認証）
            return false;
        }
    
        /**
         * ユーザーのパスワードが再ハッシュ化が必要かチェック
         */
        public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false)
        {
            // 今回は不要（パスワード認証）
        }
    }
    ```
    
1. 作成した認証プロバイダーをプロバイダーとして使える用意登録

    ```app/Providers/AppServiceProvider.php
    <?php
    
    namespace App\Providers;
    
    use App\Auth\SessionUserProvider;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\ServiceProvider;
    
    class AppServiceProvider extends ServiceProvider
    {
        /**
         * Register any application services.
         */
        public function register(): void
        {
            //
        }
    
        /**
         * Bootstrap any application services.
         */
        public function boot(): void
        {
            Auth::provider('session', function ($app, array $config) {
                return new SessionUserProvider();
            });
        }
    }
    ```

1. 作成した認証プロバイダーの設定を設定ファイルに記載

    ```config/auth.php
    <?php
    
    return [
    
        /*
        |--------------------------------------------------------------------------
        | Authentication Defaults
        |--------------------------------------------------------------------------
        |
        | This option defines the default authentication "guard" and password
        | reset "broker" for your application. You may change these values
        | as required, but they're a perfect start for most applications.
        |
        */
    
        'defaults' => [
            'guard' => env('AUTH_GUARD', 'web'),
            'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
        ],
    
        /*
        |--------------------------------------------------------------------------
        | Authentication Guards
        |--------------------------------------------------------------------------
        |
        | Next, you may define every authentication guard for your application.
        | Of course, a great default configuration has been defined for you
        | which utilizes session storage plus the Eloquent user provider.
        |
        | All authentication guards have a user provider, which defines how the
        | users are actually retrieved out of your database or other storage
        | system used by the application. Typically, Eloquent is utilized.
        |
        | Supported: "session"
        |
        */
    
        'guards' => [
            'web' => [
                'driver' => 'session',
                'provider' => 'session_users',
            ],
        ],
    
        /*
        |--------------------------------------------------------------------------
        | User Providers
        |--------------------------------------------------------------------------
        |
        | All authentication guards have a user provider, which defines how the
        | users are actually retrieved out of your database or other storage
        | system used by the application. Typically, Eloquent is utilized.
        |
        | If you have multiple user tables or models you may configure multiple
        | providers to represent the model / table. These providers may then
        | be assigned to any extra authentication guards you have defined.
        |
        | Supported: "database", "eloquent"
        |
        */
    
        'providers' => [
            'users' => [
                'driver' => 'eloquent',
                'model' => env('AUTH_MODEL', App\Models\User::class),
            ],
        
            'session_users' => [
                'driver' => 'session',
            ],
        
            // 'users' => [
            //     'driver' => 'database',
            //     'table' => 'users',
            // ],
        ],
    
        /*
        |--------------------------------------------------------------------------
        | Resetting Passwords
        |--------------------------------------------------------------------------
        |
        | These configuration options specify the behavior of Laravel's password
        | reset functionality, including the table utilized for token storage
        | and the user provider that is invoked to actually retrieve users.
        |
        | The expiry time is the number of minutes that each reset token will be
        | considered valid. This security feature keeps tokens short-lived so
        | they have less time to be guessed. You may change this as needed.
        |
        | The throttle setting is the number of seconds a user must wait before
        | generating more password reset tokens. This prevents the user from
        | quickly generating a very large amount of password reset tokens.
        |
        */
    
        'passwords' => [
            'users' => [
                'provider' => 'users',
                'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
                'expire' => 60,
                'throttle' => 60,
            ],
        ],
    
        /*
        |--------------------------------------------------------------------------
        | Password Confirmation Timeout
        |--------------------------------------------------------------------------
        |
        | Here you may define the amount of seconds before a password confirmation
        | window expires and users are asked to re-enter their password via the
        | confirmation screen. By default, the timeout lasts for three hours.
        |
        */
    
        'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
    
    ];
    ```

1. 「認証済みだがセッションにgoogle_idなどがない」場合を想定してミドルウェアを作成

    ```app/Http/Middleware/SessionIntegrityCheck.php
    <?php
    
    namespace App\Http\Middleware;
    
    use Closure;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;
    
    class SessionIntegrityCheck
    {
        public function handle(Request $request, Closure $next)
        {
            $requiredKeys = ['google_id', 'user_name', 'user_email'];
            
            foreach ($requiredKeys as $key) {
                if (!session()->has($key)) {
                    Auth::logout();
                    session()->invalidate();
                    session()->regenerateToken();
                    
                    return redirect()->route('google.redirect');
                }
            }
            
            return $next($request);
        }   
    }
    ```

1. 作成したミドルウェアを登録

    ```bootstrap/app.php
    <?php
    
    use App\Http\Middleware\SessionIntegrityCheck;
    use Illuminate\Foundation\Application;
    use Illuminate\Foundation\Configuration\Exceptions;
    use Illuminate\Foundation\Configuration\Middleware;
    
    return Application::configure(basePath: dirname(__DIR__))
        ->withRouting(
            web: __DIR__.'/../routes/web.php',
            api: __DIR__.'/../routes/api.php',
            commands: __DIR__.'/../routes/console.php',
            health: '/up',
        )
        ->withMiddleware(function (Middleware $middleware) {
            //
            $middleware->alias([
                'session.integrity' => SessionIntegrityCheck::class,
            ]);
        })
        ->withExceptions(function (Exceptions $exceptions) {
            //
        })->create();
    ```

1. ミドルウェアを使用

    ```routes/web.php
    <?php
    
    use App\Http\Controllers\OauthController;
    use Illuminate\Support\Facades\Route;
    
    Route::get('/', function () {
        return view('welcome');
    })->name('home');
    
    // Googleの認証ページへのリダイレクト
    Route::get('/auth/redirect', [OauthController::class, 'redirectToGoogle'])->name('google.redirect');
    
    // Googleからのコールバックを受け取る
    Route::get('/auth/callback', [OauthController::class, 'handleGoogleCallback'])->name('google.callback');
    
    Route::middleware(['auth', 'session.integrity'])->group(function () {
        Route::get('/dashboard', function () {
            return view('dashboard');
        })->name('dashboard');
        Route::get('/google-cloud-storage-infos', [OauthController::class, 'googleCloudStorageInfos'])->name('google.cloud.storage.infs');
        Route::get('/google-drive-infos', [OauthController::class, 'googleDriveInfos'])->name('google.drive.infos');
    });
    ```

1. ログインがセッションを用いた認証なのに、ルーティングでの認証ルート群の設定がsanctumガードを使う事になっているため修正

    ```config/jetstream.php
    <?php
    
    use Laravel\Jetstream\Features;
    use Laravel\Jetstream\Http\Middleware\AuthenticateSession;
    
    return [
    
        /*
        |--------------------------------------------------------------------------
        | Jetstream Stack
        |--------------------------------------------------------------------------
        |
        | This configuration value informs Jetstream which "stack" you will be
        | using for your application. In general, this value is set for you
        | during installation and will not need to be changed after that.
        |
        */
    
        'stack' => 'livewire',
    
        /*
        |--------------------------------------------------------------------------
        | Jetstream Route Middleware
        |--------------------------------------------------------------------------
        |
        | Here you may specify which middleware Jetstream will assign to the routes
        | that it registers with the application. When necessary, you may modify
        | these middleware; however, this default value is usually sufficient.
        |
        */
    
        'middleware' => ['web'],
    
        'auth_session' => AuthenticateSession::class,
    
        /*
        |--------------------------------------------------------------------------
        | Jetstream Guard
        |--------------------------------------------------------------------------
        |
        | Here you may specify the authentication guard Jetstream will use while
        | authenticating users. This value should correspond with one of your
        | guards that is already present in your "auth" configuration file.
        |
        */
    
        'guard' => 'web',
    
        /*
        |--------------------------------------------------------------------------
        | Features
        |--------------------------------------------------------------------------
        |
        | Some of Jetstream's features are optional. You may disable the features
        | by removing them from this array. You're free to only remove some of
        | these features or you can even remove all of these if you need to.
        |
        */
    
        'features' => [
            // Features::termsAndPrivacyPolicy(),
            // Features::profilePhotos(),
            // Features::api(),
            // Features::teams(['invitations' => true]),
            Features::accountDeletion(),
        ],
    
        /*
        |--------------------------------------------------------------------------
        | Profile Photo Disk
        |--------------------------------------------------------------------------
        |
        | This configuration value determines the default disk that will be used
        | when storing profile photos for your application's users. Typically
        | this will be the "public" disk but you may adjust this if needed.
        |
        */
    
        'profile_photo_disk' => 'public',
    
    ];
    ```

1. セッション管理がdatabaseだと非同期書き込み、及びI/Oの関係からうまくいかなかったのでfileに変更

    ```.env
    SESSION_DRIVER=file
    ```

1. .env変更後、下記を実行してconfigのキャッシュクリア

    ```
    sail artisan config:clear
    ```

1. コントローラーを修正

    ```app/Http/Controllers/OauthController.php
    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\OauthToken;
    use App\Auth\SessionUser;
    use Carbon\CarbonImmutable;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Log;
    use Laravel\Socialite\Facades\Socialite;
    use Illuminate\Http\RedirectResponse;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Http;
    
    class OauthController extends Controller
    {
    
        public function __construct(protected OauthToken $oauthToken)
        {
                
        }
    
        /**
         * Googleの認証ページへのリダイレクトを行う
         *
         * @return RedirectResponse Googleの認証ページへのリダイレクトレスポンス
         */
        public function redirectToGoogle(): RedirectResponse
        {
            return Socialite::driver('google')
                ->scopes(['email', 'profile',
                    'https://www.googleapis.com/auth/cloud-platform.read-only',
                    'https://www.googleapis.com/auth/drive.readonly',
                    'https://www.googleapis.com/auth/userinfo.profile',
                ])
                ->with(['access_type' => 'offline', 'prompt' => 'consent'])
                ->redirect();
        }
    
        /**
         * Googleからのコールバックを受け取る
         *
         * @return RedirectResponse リダイレクトレスポンス
         */
        public function handleGoogleCallback(): RedirectResponse
        {
            try {
                $googleUser = Socialite::driver('google')->user();
            
                $this->storeUserSessionData($googleUser);
                $this->loginSessionUser($googleUser->getId());
                $this->saveOauthToken($googleUser);
            
            } catch (\Exception $e) {
                Log::error('Google認証コールバック処理中にエラーが発生しました', ['exception' => $e]);
                return redirect(route('home'))->with('error', '認証処理中にエラーが発生しました');
            }
            
            return redirect('/dashboard');
        }
    
        public function googleCloudStorageInfos()
        {
            $redirect = $this->verifyOauthToken();
            if ($redirect) {
                return $redirect;
            }
        
            $oauthToken = $this->oauthToken->where('user_id', Auth::user()->id)->first();
            $accessToken = $oauthToken->access_token;
        
            try {
                $response = Http::withToken($accessToken)->get('https://www.googleapis.com/storage/v1/b', [
                    'project' => 'Google CloudのプロジェクトID' // .envで持ちたい
                ]);
            
                if ($response->successful()) {
                    $buckets = $response->json();
                    return response()->json($buckets);
                } else {
                    Log::error('Google Cloud API request failed', ['response' => $response->body()]);
                    return response()->json(['error' => 'Failed to retrieve Google Cloud resources'], 500);
                }
            } catch (\Exception $e) {
                Log::error('Exception occurred while fetching Google Cloud resources', ['exception' => $e]);
                return response()->json(['error' => 'An error occurred while fetching Google Cloud resources'], 500);
            }
        }
    
        public function googleDriveInfos()
        {
            $redirect = $this->verifyOauthToken();
            if ($redirect) {
                return $redirect;
            }
        
            $oauthToken = $this->oauthToken->where('user_id', Auth::user()->id)->first();
            $accessToken = $oauthToken->access_token;
        
            try {
                $response = Http::withToken($accessToken)->get('https://www.googleapis.com/drive/v3/files?q=\'me\' in owners&fields=files(id, name)');
            
                if ($response->successful()) {
                    $buckets = $response->json();
                    return response()->json($buckets);
                } else {
                    Log::error('Google Cloud API request failed', ['response' => $response->body()]);
                    return response()->json(['error' => 'Failed to retrieve Google Cloud resources'], 500);
                }
            } catch (\Exception $e) {
                Log::error('Exception occurred while fetching Google Cloud resources', ['exception' => $e]);
                return response()->json(['error' => 'An error occurred while fetching Google Cloud resources'], 500);
            }
        }
    
        /**
         * セッションにユーザー情報を保存
         */
        private function storeUserSessionData($googleUser): void
        {
            session([
                'google_id' => $googleUser->getId(),
                'user_name' => $googleUser->getName(),
                'user_email' => $googleUser->getEmail(),
                'avatar' => $googleUser->getAvatar(),
            ]);
        }
    
        /**
         * SessionUserでログイン処理
         */
        private function loginSessionUser(string $googleId): void
        {
            $sessionUser = new SessionUser($googleId);
            Auth::login($sessionUser);
        }
    
        /**
         * OAuthトークンをデータベースに保存
         */
        private function saveOauthToken($googleUser): void
        {
            try {
                DB::beginTransaction();
                // 同じgoogle_idのレコードがあったら削除
                $this->oauthToken->where('google_id', $googleUser->getId())->delete();
            
                $now = CarbonImmutable::now();
                $accessTokenExpiresAt = $now->addSeconds($googleUser->expiresIn);
                $refreshTokenExpiresAt = null; // NOTE: Googleの認可サーバーはrefresh_tokenの有効期限を返さない
            
                // 新しいトークンを挿入
                $this->oauthToken->create([
                    'google_id' => $googleUser->getId(),
                    'access_token' => $googleUser->token,
                    'refresh_token' => $googleUser->refreshToken ?? null,
                    'access_token_expires_at' => $accessTokenExpiresAt,
                    'refresh_token_expires_at' => $refreshTokenExpiresAt,
                ]);
                
                DB::commit();
            } catch (\Exception $e) {
                DB::rollBack();
                Log::error('トークンの保存中にエラーが発生しました', ['exception' => $e]);
                throw $e;
            }
        }
    
        /**
         * OAuth系トークンの確認
         */
        private function verifyOauthToken()
        {
            if (!$this->oauthToken->where('google_id', session('google_id'))->exists()) {
                Auth::guard('web')->logout();
                return redirect()->route('google.redirect');
            }
        
            if ($this->isExpirationAccessToken()) {
                if ($this->oauthToken->where('google_id', session('google_id'))->whereNotNull('refresh_token')->exists()) {
                    $this->refreshAccessToken();
                } else {
                    Auth::guard('web')->logout();
                    return redirect()->route('google.redirect');
                }
            }
        
            return null;
        }
    
        /**
         * アクセストークンの有効期限切れ確認
         *
         * @return bool 有効期限切れの場合true、有効期限内の場合false
         */
        private function isExpirationAccessToken(): bool
        {
            $oauthToken = $this->oauthToken->where('google_id', session('google_id'))->first();
            $accessTokenExpiresAt = $oauthToken->access_token_expires_at;
            $now = CarbonImmutable::now();
            return $now->greaterThan($accessTokenExpiresAt);
        }
    
    
        /**
         * アクセストークンの更新
         */
        private function refreshAccessToken()
        {
            $oauthToken = $this->oauthToken->where('google_id', session('google_id'))->first();
            $refreshToken = $oauthToken->refresh_token;
        
            $response = Http::post('https://oauth2.googleapis.com/token', [
                'client_id' => config('services.google.client_id'),
                'client_secret' => config('services.google.client_secret'),
                'refresh_token' => $refreshToken,
                'grant_type' => 'refresh_token',
            ]);
        
            if ($response->successful()) {
                $oauthToken->update([
                    'access_token' => $response->json()['access_token'],
                    'access_token_expires_at' => CarbonImmutable::now()->addSeconds($response->json()['expires_in']),
                ]);
            } else {
                Log::error('Failed to refresh access token', ['response' => $response->body()]);
                return redirect()->route('google.redirect');
            }
        }
    }
    ```

1. ログイン後のヘッダ周りでデータが取れなくなっている物があるので修正

    ```/resources/views/navigation-menu.blade.php
    <nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
        <!-- Primary Navigation Menu -->
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16">
                <div class="flex">
                    <!-- Logo -->
                    <div class="shrink-0 flex items-center">
                        <a href="{{ route('dashboard') }}">
                            <x-application-mark class="block h-9 w-auto" />
                        </a>
                    </div>
                  
                    <!-- Navigation Links -->
                    <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
                        <x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                            {{ __('Dashboard') }}
                        </x-nav-link>
                        <x-nav-link href="{{ route('google.cloud.storage.infs') }}" :active="request()->routeIs('google.cloud.storage.infs')">
                            {{ __('Google Cloud Storage Infos') }}
                        </x-nav-link>
                        <x-nav-link href="{{ route('google.drive.infos') }}" :active="request()->routeIs('google.drive.infos')">
                            {{ __('Google Drive Infos') }}
                        </x-nav-link>
                    </div>
                </div>
              
                <div class="hidden sm:flex sm:items-center sm:ms-6">
                    <!-- Teams Dropdown -->
                    @if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
                        <div class="ms-3 relative">
                            <x-dropdown align="right" width="60">
                                <x-slot name="trigger">
                                    <span class="inline-flex rounded-md">
                                        <button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
                                            {{ Auth::user()->currentTeam?->name ?? 'No Team' }}
                                        
                                            <svg class="ms-2 -me-0.5 size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
                                                <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
                                            </svg>
                                        </button>
                                    </span>
                                </x-slot>
                              
                                <x-slot name="content">
                                    <div class="w-60">
                                        <!-- Team Management -->
                                        <div class="block px-4 py-2 text-xs text-gray-400">
                                            {{ __('Manage Team') }}
                                        </div>
                                      
                                        <!-- Team Settings -->
                                        @if(Auth::user()->currentTeam)
                                <x-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
                                            {{ __('Team Settings') }}
                                        </x-dropdown-link>
                                        @endif
                                      
                                        @can('create', Laravel\Jetstream\Jetstream::newTeamModel())
                                            <x-dropdown-link href="{{ route('teams.create') }}">
                                                {{ __('Create New Team') }}
                                            </x-dropdown-link>
                                        @endcan
                                          
                                        <!-- Team Switcher -->
                                        @if (Auth::user() && Auth::user()->allTeams()->count() > 1)
                                            <div class="border-t border-gray-200"></div>
                                          
                                            <div class="block px-4 py-2 text-xs text-gray-400">
                                                {{ __('Switch Teams') }}
                                            </div>
                                          
                                            @foreach (Auth::user()->allTeams() as $team)
                                                <x-switchable-team :team="$team" />
                                            @endforeach
                                        @endif
                                    </div>
                                </x-slot>
                            </x-dropdown>
                        </div>
                    @endif
                      
                    <!-- Settings Dropdown -->
                    <div class="ms-3 relative">
                        <x-dropdown align="right" width="48">
                            <x-slot name="trigger">
                                @if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
                                    <button class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
                                        <img class="size-8 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
                                    </button>
                                @else
                                    <span class="inline-flex rounded-md">
                                        <button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition ease-in-out duration-150">
                                            {{ Auth::user()->name }}
                                        
                                            <svg class="ms-2 -me-0.5 size-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
                                                <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
                                            </svg>
                                        </button>
                                    </span>
                                @endif
                            </x-slot>
                          
                            <x-slot name="content">
                                <!-- Account Management -->
                                <div class="block px-4 py-2 text-xs text-gray-400">
                                    {{ __('Manage Account') }}
                                </div>
                              
                                <x-dropdown-link href="{{ route('profile.show') }}">
                                    {{ __('Profile') }}
                                </x-dropdown-link>
                              
                                @if (Laravel\Jetstream\Jetstream::hasApiFeatures())
                                    <x-dropdown-link href="{{ route('api-tokens.index') }}">
                                        {{ __('API Tokens') }}
                                    </x-dropdown-link>
                                @endif
                                  
                                <div class="border-t border-gray-200"></div>
                                  
                                <!-- Authentication -->
                                <form method="POST" action="{{ route('logout') }}" x-data>
                                    @csrf
                                
                                    <x-dropdown-link href="{{ route('logout') }}"
                                             @click.prevent="$root.submit();">
                                        {{ __('Log Out') }}
                                    </x-dropdown-link>
                                </form>
                            </x-slot>
                        </x-dropdown>
                    </div>
                </div>
              
                <!-- Hamburger -->
                <div class="-me-2 flex items-center sm:hidden">
                    <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
                        <svg class="size-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                            <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                            <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                        </svg>
                    </button>
                </div>
            </div>
        </div>
      
        <!-- Responsive Navigation Menu -->
        <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
            <div class="pt-2 pb-3 space-y-1">
                <x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                    {{ __('Dashboard') }}
                </x-responsive-nav-link>
            </div>
          
            <!-- Responsive Settings Options -->
            <div class="pt-4 pb-1 border-t border-gray-200">
                <div class="flex items-center px-4">
                    @if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
                        <div class="shrink-0 me-3">
                            <img class="size-10 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
                        </div>
                    @endif
                      
                    <div>
                        <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
                        <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
                    </div>
                </div>
              
                <div class="mt-3 space-y-1">
                    <!-- Account Management -->
                    <x-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
                        {{ __('Profile') }}
                    </x-responsive-nav-link>
                  
                    @if (Laravel\Jetstream\Jetstream::hasApiFeatures())
                        <x-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
                            {{ __('API Tokens') }}
                        </x-responsive-nav-link>
                    @endif
                      
                    <!-- Authentication -->
                    <form method="POST" action="{{ route('logout') }}" x-data>
                        @csrf
                    
                        <x-responsive-nav-link href="{{ route('logout') }}"
                                       @click.prevent="$root.submit();">
                            {{ __('Log Out') }}
                        </x-responsive-nav-link>
                    </form>
                  
                    <!-- Team Management -->
                    @if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
                        <div class="border-t border-gray-200"></div>
                  
                        <div class="block px-4 py-2 text-xs text-gray-400">
                            {{ __('Manage Team') }}
                        </div>
                      
                        <!-- Team Settings -->
                        @if(Auth::user()->currentTeam)
                        <x-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
                            {{ __('Team Settings') }}
                        </x-responsive-nav-link>
                        @endif
                      
                        @can('create', Laravel\Jetstream\Jetstream::newTeamModel())
                            <x-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
                                {{ __('Create New Team') }}
                            </x-responsive-nav-link>
                        @endcan
                          
                        <!-- Team Switcher -->
                        @if (Auth::user() && Auth::user()->allTeams()->count() > 1)
                            <div class="border-t border-gray-200"></div>
                          
                            <div class="block px-4 py-2 text-xs text-gray-400">
                                {{ __('Switch Teams') }}
                            </div>
                          
                            @foreach (Auth::user()->allTeams() as $team)
                                <x-switchable-team :team="$team" component="responsive-nav-link" />
                            @endforeach
                        @endif
                    @endif
                </div>
            </div>
        </div>
    </nav>
    ```

1. ログインできることを画面から確認、認証状態でDBを覗いてusersテーブルにレコードが作られないことを確認