> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth 2.0 驗證 - Google Sheets API for Laravel

> Google Sheets API 的 OAuth 2.0 驗證。適合使用者個別存取。

OAuth 2.0 驗證讓使用者個別授權存取自己的 Google Sheets。適合處理使用者個別資料的應用程式。

## 適用場景

* **以使用者為中心的應用** — 使用者存取自己的 Google Sheets
* **多租戶應用** — 不同使用者管理不同試算表
* **個人資料存取** — 讀寫使用者個別帳戶的工作表
* **桌面/Web 應用** — 具備使用者互動的應用

## 前置條件

* Google Cloud Console 專案
* 啟用 Google Sheets API 與 Google Drive API
* Laravel Socialite(建議)

## 設定

<Steps>
  <Step title="設定 Google Cloud Console">
    1. 前往 [Google Cloud Console](https://console.cloud.google.com/)
    2. 選擇專案或新建
    3. 前往 **APIs & Services** > **Library**
    4. 啟用下列 API:
       * **Google Sheets API**
       * **Google Drive API**
  </Step>

  <Step title="建立 OAuth 2.0 憑證">
    1. 前往 **APIs & Services** > **Credentials**
    2. 點擊 **Create Credentials** > **OAuth client ID**
    3. 設定 OAuth 同意畫面(僅初次):
       * 選擇使用者類型為 **External**
       * 填寫必填項目(應用程式名稱、支援電子郵件、開發者聯絡資訊)
       * 加入 scope:`https://www.googleapis.com/auth/spreadsheets` 與 `https://www.googleapis.com/auth/drive`
    4. 應用程式類型選擇 **Web application**
    5. 加入授權重新導向 URI:
       * 開發環境: `http://localhost:8000/auth/google/callback`
       * 正式環境: `https://yourdomain.com/auth/google/callback`
    6. 點擊 **Create**
    7. 複製 **Client ID** 與 **Client Secret**
  </Step>

  <Step title="設定 Laravel 環境">
    在 `.env` 檔案加入:

    ```env theme={null}
    GOOGLE_CLIENT_ID=your-client-id-here
    GOOGLE_CLIENT_SECRET=your-client-secret-here
    GOOGLE_REDIRECT=http://localhost:8000/auth/google/callback
    ```

    更新 `config/google.php`:

    ```php theme={null}
    'client_id' => env('GOOGLE_CLIENT_ID', ''),
    'client_secret' => env('GOOGLE_CLIENT_SECRET', ''),
    'redirect_uri' => env('GOOGLE_REDIRECT', ''),
    'scopes' => [
        \Google\Service\Sheets::SPREADSHEETS,
        \Google\Service\Drive::DRIVE,
    ],
    'access_type' => 'offline', // refresh token 必填
    'prompt' => 'consent select_account',
    ```
  </Step>

  <Step title="安裝 Laravel Socialite">
    ```bash theme={null}
    composer require laravel/socialite
    ```

    在 `config/services.php` 加入:

    ```php theme={null}
    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect' => env('GOOGLE_REDIRECT'),
    ],
    ```
  </Step>

  <Step title="實作驗證 Controller">
    ```php theme={null}
    // app/Http/Controllers/AuthController.php
    <?php

    namespace App\Http\Controllers;

    use App\Models\User;
    use Illuminate\Http\Request;
    use Laravel\Socialite\Facades\Socialite;

    class AuthController extends Controller
    {
        public function redirectToGoogle()
        {
            return Socialite::driver('google')
                ->scopes(config('google.scopes'))
                ->with([
                    'access_type' => config('google.access_type'),
                    'prompt' => config('google.prompt'),
                ])
                ->redirect();
        }

        public function handleGoogleCallback()
        {
            try {
                $googleUser = Socialite::driver('google')->user();
                
                $user = User::updateOrCreate(
                    ['email' => $googleUser->email],
                    [
                        'name' => $googleUser->name,
                        'email' => $googleUser->email,
                        'google_access_token' => $googleUser->token,
                        'google_refresh_token' => $googleUser->refreshToken,
                        'google_expires_in' => $googleUser->expiresIn,
                        'google_token_created' => now()->timestamp,
                    ]
                );

                auth()->login($user);

                return redirect('/dashboard')->with('success', 'Google 驗證成功');
            } catch (\Exception $e) {
                return redirect('/login')
                    ->with('error', '驗證失敗: ' . $e->getMessage());
            }
        }

        public function logout(Request $request)
        {
            auth()->logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect('/');
        }
    }
    ```
  </Step>

  <Step title="新增路由">
    ```php theme={null}
    // routes/web.php
    Route::get('/auth/google', [AuthController::class, 'redirectToGoogle'])
        ->name('google.redirect');
    Route::get('/auth/google/callback', [AuthController::class, 'handleGoogleCallback'])
        ->name('google.callback');
    Route::post('/logout', [AuthController::class, 'logout'])
        ->name('logout');
    ```
  </Step>

  <Step title="更新 User 模型">
    Migration:

    ```php theme={null}
    // database/migrations/add_google_tokens_to_users_table.php
    Schema::table('users', function (Blueprint $table) {
        $table->text('google_access_token')->nullable();
        $table->text('google_refresh_token')->nullable();
        $table->integer('google_expires_in')->nullable();
        $table->integer('google_token_created')->nullable();
    });
    ```

    User 模型:

    ```php theme={null}
    // app/Models/User.php
    protected $fillable = [
        'name',
        'email',
        'password',
        'google_access_token',
        'google_refresh_token',
        'google_expires_in',
        'google_token_created',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'google_access_token',
        'google_refresh_token',
    ];

    public function getGoogleTokenArray(): array
    {
        return [
            'access_token' => $this->google_access_token,
            'refresh_token' => $this->google_refresh_token,
            'expires_in' => $this->google_expires_in,
            'created' => $this->google_token_created,
        ];
    }

    public function hasValidGoogleToken(): bool
    {
        return !empty($this->google_access_token) 
            && !empty($this->google_refresh_token);
    }
    ```
  </Step>

  <Step title="使用 Sheets">
    ```php theme={null}
    use Revolution\Google\Sheets\Facades\Sheets;

    public function getSheetData(Request $request)
    {
        $user = $request->user();
        
        if (!$user->hasValidGoogleToken()) {
            return redirect()->route('google.redirect');
        }

        try {
            $token = $user->getGoogleTokenArray();

            $values = Sheets::setAccessToken($token)
                ->spreadsheet('user-spreadsheet-id')
                ->sheet('Sheet1')
                ->all();
                
            return view('sheets.data', compact('values'));
        } catch (\Exception $e) {
            // token 過期時重新驗證
            if (str_contains($e->getMessage(), 'invalid_grant') 
                || str_contains($e->getMessage(), 'unauthorized')) {
                return redirect()->route('google.redirect');
            }

            throw $e;
        }
    }
    ```
  </Step>
</Steps>

## Token 更新

套件會自動處理 token 過期:

```php theme={null}
$token = [
    'access_token' => $user->google_access_token,
    'refresh_token' => $user->google_refresh_token,
    'expires_in' => $user->google_expires_in,
    'created' => $user->google_token_created,
];

// 過期時自動 refresh
Sheets::setAccessToken($token)
    ->spreadsheet('id')
    ->sheet('Sheet1')
    ->all();

// 取得 refresh 後更新的 token
$updatedToken = Sheets::getAccessToken();
if ($updatedToken) {
    $user->update([
        'google_access_token' => $updatedToken['access_token'],
        'google_token_created' => time(),
    ]);
}
```

## Middleware

要求 Google 驗證的 middleware:

```php theme={null}
// app/Http/Middleware/RequireGoogleAuth.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RequireGoogleAuth
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();

        if (!$user || !$user->hasValidGoogleToken()) {
            if ($request->expectsJson()) {
                return response()->json(
                    ['error' => '需要 Google 驗證'], 
                    401
                );
            }

            return redirect()->route('google.redirect');
        }

        return $next($request);
    }
}
```

## 安全性

### 1. Token 儲存

* 將 token 安全地儲存於資料庫
* 使用 Laravel 內建加密
* 不要將 token 公開至客戶端

### 2. Scope 管理

* 僅要求必要的最小 scope
* 套用最小權限原則
* 向使用者清楚說明所需權限

### 3. 錯誤處理

* 適當處理過期 token
* 提供使用者友善的重新驗證流程
* 記錄與監控錯誤

## 疑難排解

### 常見錯誤

**「redirect\_uri\_mismatch」錯誤**

* 確認 Google Console 的 redirect URI 與應用程式完全相符
* 確認 http vs https 差異
* 確認結尾斜線

**「invalid\_grant」或「unauthorized」錯誤**

* token 過期且 refresh 失敗
* 要求使用者重新驗證
* 確認 refresh token 是否存在

**「access\_denied」錯誤**

* 使用者拒絕授權
* 以適當訊息因應
* 提供重試驗證的選項

### 測試路由

```php theme={null}
Route::get('/test-oauth', function (Request $request) {
    $user = $request->user();
    
    if (!$user->hasValidGoogleToken()) {
        return '沒有 Google token。'
            . '<a href="' . route('google.redirect') . '">'
            . '驗證</a>';
    }
    
    try {
        $token = $user->getGoogleTokenArray();
        $sheets = Sheets::setAccessToken($token)->spreadsheetList();
        
        return 'OAuth 運作中!試算表數量: ' 
            . count($sheets);
    } catch (\Exception $e) {
        return 'OAuth 錯誤: ' . $e->getMessage();
    }
})->middleware('auth');
```


## Related topics

- [Google Sheets API for Laravel](/zh-TW/packages/laravel-google-sheets/index.md)
- [Service Account 驗證 - Google Sheets API for Laravel](/zh-TW/packages/laravel-google-sheets/service-account.md)
- [驗證](/zh-TW/validation.md)
- [自訂驗證 Guard 的實作](/zh-TW/advanced/custom-auth-guard.md)
- [Laravel Socialite（社群認證）](/zh-TW/socialite.md)
