OAuth 2.0 authentication allows users to grant your application access to their personal Google Sheets. This method is ideal for user-facing applications where each user manages their own spreadsheets.
When to Use OAuth 2.0
- User-centric applications — Users access their own Google Sheets
- Multi-tenant applications — Different users access different spreadsheets
- Personal data access — Reading/writing user’s personal Google account data
- Web applications — Apps where user interaction is available
Prerequisites
- Google Cloud Console project
- Google Sheets API and Google Drive API enabled
- Laravel Socialite package (recommended)
Setup Steps
Configure Google Cloud Console
- Go to Google Cloud Console
- Select your project or create a new one
- Navigate to APIs & Services > Library
- Enable:
- Google Sheets API
- Google Drive API
Create OAuth 2.0 Credentials
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Configure OAuth consent screen (first time only):
- Choose External for public apps
- Fill required fields (app name, support email, developer contact)
- Add scopes:
https://www.googleapis.com/auth/spreadsheets and https://www.googleapis.com/auth/drive
- Select Web application as application type
- Add Authorized Redirect URIs:
- Development:
http://localhost:8000/auth/google/callback
- Production:
https://yourdomain.com/auth/google/callback
- Click Create
- Copy your Client ID and Client Secret
Configure Laravel Environment
Add to .env:GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
GOOGLE_REDIRECT=http://localhost:8000/auth/google/callback
Update config/google.php:'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', // Required for refresh tokens
'prompt' => 'consent select_account',
Install Laravel Socialite
composer require laravel/socialite
Add to config/services.php:'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT'),
],
Implement OAuth Flow
Create an authentication controller:// 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', 'Successfully connected to Google!');
} catch (\Exception $e) {
return redirect('/login')
->with('error', 'Authentication failed: ' . $e->getMessage());
}
}
public function logout(Request $request)
{
auth()->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
Add Routes
// 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');
Update User Model
Create migration:// 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();
});
Update User model:// 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);
}
Use Sheets with OAuth
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 expired, redirect to re-authenticate
if (str_contains($e->getMessage(), 'invalid_grant')
|| str_contains($e->getMessage(), 'unauthorized')) {
return redirect()->route('google.redirect');
}
throw $e;
}
}
Token Refresh
The package automatically handles token refresh when expired:
$token = [
'access_token' => $user->google_access_token,
'refresh_token' => $user->google_refresh_token,
'expires_in' => $user->google_expires_in,
'created' => $user->google_token_created,
];
// Automatically refreshes if expired
Sheets::setAccessToken($token)
->spreadsheet('id')
->sheet('Sheet1')
->all();
// Get updated token after refresh
$updatedToken = Sheets::getAccessToken();
if ($updatedToken) {
$user->update([
'google_access_token' => $updatedToken['access_token'],
'google_token_created' => time(),
]);
}
Middleware
Create middleware to require Google authentication:
// 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 authentication required'],
401
);
}
return redirect()->route('google.redirect');
}
return $next($request);
}
}
Security Considerations
1. Token Storage
- Store tokens securely in the database
- Use Laravel’s built-in encryption
- Never expose tokens in client-side code
2. Scope Management
- Only request necessary scopes
- Follow the principle of least privilege
- Clearly explain to users what access you need
3. Error Handling
- Handle expired tokens gracefully
- Provide clear re-authentication flows
- Log authentication errors for monitoring
Troubleshooting
Common Errors
“redirect_uri_mismatch” error
- Verify redirect URI in Google Console matches exactly
- Check for http vs https differences
- Verify trailing slashes match
“invalid_grant” or “unauthorized” error
- Token has expired and refresh failed
- Redirect user to re-authenticate
- Check if refresh token is available
“access_denied” error
- User denied permission
- Handle gracefully with appropriate messaging
- Provide option to retry authentication
Test OAuth Setup
Create a test route to verify your OAuth configuration:
Route::get('/test-oauth', function (Request $request) {
$user = $request->user();
if (!$user->hasValidGoogleToken()) {
return 'No Google token available. '
. '<a href="' . route('google.redirect') . '">'
. 'Authenticate</a>';
}
try {
$token = $user->getGoogleTokenArray();
$sheets = Sheets::setAccessToken($token)->spreadsheetList();
return 'OAuth working! Found ' . count($sheets)
. ' spreadsheets.';
} catch (\Exception $e) {
return 'OAuth error: ' . $e->getMessage();
}
})->middleware('auth');
Last modified on June 14, 2026