> ## 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 동의 화면 설정(최초 1회):
       * 사용자 타입을 **External**로 선택
       * 필수 항목 입력(앱 이름, 지원 이메일, 개발자 연락처)
       * 스코프 추가: `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', // 리프레시 토큰에 필수
    '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="인증 컨트롤러 구현">
    ```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 모델 업데이트">
    마이그레이션:

    ```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) {
            // 토큰 만료 시 재인증으로
            if (str_contains($e->getMessage(), 'invalid_grant') 
                || str_contains($e->getMessage(), 'unauthorized')) {
                return redirect()->route('google.redirect');
            }

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

## 토큰 리프레시

패키지는 토큰 유효 기간 만료를 자동으로 처리합니다:

```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,
];

// 만료 시 자동으로 리프레시
Sheets::setAccessToken($token)
    ->spreadsheet('id')
    ->sheet('Sheet1')
    ->all();

// 리프레시 후 갱신 토큰을 취득
$updatedToken = Sheets::getAccessToken();
if ($updatedToken) {
    $user->update([
        'google_access_token' => $updatedToken['access_token'],
        'google_token_created' => time(),
    ]);
}
```

## 미들웨어

Google 인증을 필수로 하는 미들웨어:

```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. 토큰 보관

* 토큰은 데이터베이스에 안전하게 저장
* Laravel의 내장 암호화 사용
* 클라이언트 측에 토큰을 노출하지 않기

### 2. 스코프 관리

* 필요 최소한의 스코프만 요청
* 최소 권한 원칙 적용
* 사용자에게 필요한 권한을 명확히 설명

### 3. 에러 처리

* 만료된 토큰을 적절히 처리
* 사용자 친화적인 재인증 흐름 제공
* 에러 로그 기록·모니터링

## 트러블슈팅

### 자주 발생하는 에러

**"redirect\_uri\_mismatch" 에러**

* Google Console의 리디렉트 URI와 애플리케이션이 완전히 일치하는지 확인
* http vs https 차이 확인
* 말미의 슬래시 확인

**"invalid\_grant" 또는 "unauthorized" 에러**

* 토큰 만료로 인해 리프레시에 실패
* 사용자에게 재인증을 요청
* 리프레시 토큰이 존재하는지 확인

**"access\_denied" 에러**

* 사용자가 권한 허용을 거부
* 적절한 메시지로 대응
* 재인증을 시도하는 옵션 제공

### 테스트 라우트

```php theme={null}
Route::get('/test-oauth', function (Request $request) {
    $user = $request->user();
    
    if (!$user->hasValidGoogleToken()) {
        return 'Google 토큰이 없습니다.'
            . '<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](/ko/packages/laravel-google-sheets/index.md)
- [Service account 인증 - Google Sheets API for Laravel](/ko/packages/laravel-google-sheets/service-account.md)
- [Laravel Socialite (소셜 인증)](/ko/socialite.md)
- [인증 방식 비교 - Laravel Bluesky](/ko/packages/laravel-bluesky/authentication.md)
- [Laravel Passport(OAuth2 서버 구현)](/ko/passport.md)
