laravel-google-passport-oauth maintained by impression
Laravel Google Passport OAuth
Exchange Google OAuth access tokens for Laravel Passport personal access tokens. Enables seamless authentication from external applications (like better-chatbot) to your Laravel MCP server.
Features
- ✅ Validate Google OAuth tokens
- ✅ Refresh expired tokens server-side
- ✅ Find existing users by email (pre-provisioning required)
- ✅ Issue Passport personal access tokens
- ✅ Configurable per-app (column mapping, domain whitelist)
- ✅ No auto-user-creation (prevents account enumeration)
- ✅ Rate limiting built-in (60 requests/minute)
- ✅ Comprehensive error handling with specific status codes
- ✅ Full test coverage (18 passing tests)
Installation
Step 1: Install via Composer
composer require impression/laravel-google-passport-oauth
Step 2: Publish Configuration
php artisan vendor:publish --provider="Impression\GooglePassportOAuth\GoogleOAuthServiceProvider" --tag="google-oauth-config"
This creates config/google-oauth.php with sensible defaults.
Step 3: Set Environment Variables
Add to .env:
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id-from-console
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret
GOOGLE_OAUTH_USER_MODEL=App\Models\User # optional, defaults to App\Models\User
GOOGLE_OAUTH_GOOGLE_ID_COLUMN=google_id # optional, for custom column name
GOOGLE_OAUTH_EMAIL_COLUMN=email # optional, for custom column name
GOOGLE_OAUTH_DOMAIN_WHITELIST=your-company.com,partner.com # optional, comma-separated
Step 4: Ensure Users Table Has Required Columns
If you need to store Google user IDs, add a migration:
Schema::table('users', function (Blueprint $table) {
$table->string('google_id')->nullable()->unique();
});
Run migrations:
php artisan migrate
Configuration
The package exposes configuration in config/google-oauth.php:
return [
// Google OAuth credentials (from Google Cloud Console)
'google_client_id' => env('GOOGLE_OAUTH_CLIENT_ID'),
'google_client_secret' => env('GOOGLE_OAUTH_CLIENT_SECRET'),
// Which user model to query for authentication
'user_model' => env('GOOGLE_OAUTH_USER_MODEL', 'App\Models\User'),
// Column mapping for flexible schema
'column_mapping' => [
'email' => env('GOOGLE_OAUTH_EMAIL_COLUMN', 'email'),
'google_id' => env('GOOGLE_OAUTH_GOOGLE_ID_COLUMN', 'google_id'),
],
// Whitelist by domain (null = allow all domains)
'domain_whitelist' => env('GOOGLE_OAUTH_DOMAIN_WHITELIST')
? explode(',', env('GOOGLE_OAUTH_DOMAIN_WHITELIST'))
: null,
// Passport token expiry (seconds)
'token_expires_in' => 31536000, // 1 year
];
Column Mapping
If your users table uses different column names:
# For a table with: google_user_id, employee_email
GOOGLE_OAUTH_GOOGLE_ID_COLUMN=google_user_id
GOOGLE_OAUTH_EMAIL_COLUMN=employee_email
Domain Whitelist
Restrict authentication to specific email domains:
GOOGLE_OAUTH_DOMAIN_WHITELIST=company.com,partner.com
Only users with email addresses ending in these domains can authenticate. Leave empty/null to allow all domains.
Usage
Authentication Flow
- External app (e.g., chatbot) obtains Google OAuth token from user
- External app sends token to your Laravel endpoint:
POST /oauth/google-token - Your package validates token with Google, finds user by email
- Your package issues Passport personal access token
- External app uses Passport token for subsequent API calls
HTTP Endpoint
POST /oauth/google-token
Request
curl -X POST http://localhost:8000/oauth/google-token \
-H "Content-Type: application/json" \
-d '{
"access_token": "ya29.a0AfH6SMBx...",
"refresh_token": "1//0gx..."
}'
Parameters:
access_token(required): Google OAuth access tokenrefresh_token(optional): Google OAuth refresh token (used if access token is expired)
Success Response (200 OK)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 31536000
}
Use the access_token as a Bearer token in subsequent API requests:
curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." \
http://localhost:8000/api/protected-endpoint
Error Responses
400 Bad Request — Validation failed
{
"error": "invalid_request",
"message": {
"access_token": ["The access_token field is required."]
}
}
401 Unauthorized — Token invalid, expired, or user not found
{
"error": "invalid_grant",
"message": "The provided token is invalid or tampered with."
}
Other 401 errors:
token_expired: Token is expired (no refresh token provided)user_not_found: User email not in databasedomain_not_allowed: User email domain not whitelisted
503 Service Unavailable — Google API error
{
"error": "service_unavailable",
"message": "Google API returned status 503"
}
User Provisioning
Users must exist in your database before authenticating. The package does not auto-create users to prevent account enumeration attacks.
Provisioning Workflow
- Add user to database (via admin panel, bulk import, or migration):
User::create([
'name' => 'John Doe',
'email' => 'john@company.com',
'google_id' => 'google-user-id-123', // optional, from Google tokeninfo
'password' => Hash::make(Str::random(32)), // optional, can be dummy
]);
- User authenticates via
POST /oauth/google-token - Package validates email matches (finds user)
- Package issues Passport token
Pre-provisioning at Scale
For bulk onboarding:
# CSV import
php artisan import:users users.csv --from-google
# Sync from directory (LDAP, Entra, etc.)
php artisan sync:users --provider=entra
Security Considerations
Production Checklist
- ✅ Use HTTPS only (enforce via middleware or load balancer)
- ✅ Store credentials in
.env(never commit to version control) - ✅ Pre-provision users carefully (audit logging recommended)
- ✅ Monitor rate limiting (
/oauth/google-tokenmax 60 requests/minute per IP) - ✅ Use domain whitelist in production
- ✅ Rotate Google OAuth credentials regularly
How It Works
- No token storage — Tokens validated in-memory, never persisted
- Server-side refresh — If token expired but refresh token provided, package refreshes silently
- Email-based lookup — User identity determined by email (not Google ID)
- Pre-provisioning only — Prevents account enumeration
- Signed Passport tokens — Subsequent API calls verified with Passport
Configuration Per Application
Each Laravel app using this package can customize the configuration independently:
MCP Server (config/google-oauth.php):
'column_mapping' => ['email' => 'email', 'google_id' => 'google_user_id'],
'domain_whitelist' => ['company.com'],
Internal App (config/google-oauth.php):
'column_mapping' => ['email' => 'work_email', 'google_id' => 'gid'],
'domain_whitelist' => ['company.com', 'internal.company.com'],
Partner Portal (config/google-oauth.php):
'column_mapping' => ['email' => 'contact_email', 'google_id' => null],
'domain_whitelist' => ['partner1.com', 'partner2.com'],
Testing
Running Tests
# All tests
vendor/bin/phpunit tests/
# Unit tests only
vendor/bin/phpunit tests/Unit/
# Feature tests only
vendor/bin/phpunit tests/Feature/
# Specific test
vendor/bin/phpunit tests/Unit/GoogleTokenValidatorTest.php
Test Coverage
- 18 tests passing (9 skipped for Passport integration)
- Unit tests for token validation and user resolution
- Feature tests for controller behavior
- Integration tests for end-to-end flow
- HTTP mocking for Google API (no external dependencies)
Writing Integration Tests
In your app, add integration tests:
use Illuminate\Support\Facades\Http;
public function test_google_oauth_integration()
{
Http::fake([
'https://www.googleapis.com/oauth2/v3/tokeninfo' => Http::response([
'email' => 'test@company.com',
'user_id' => 'google-123',
'expires_in' => 3600,
]),
]);
// Create user
$user = User::factory()->create(['email' => 'test@company.com']);
// Exchange token
$response = $this->postJson('/oauth/google-token', [
'access_token' => 'test-token',
]);
$response->assertStatus(200)
->assertJsonStructure(['access_token', 'token_type', 'expires_in']);
}
Quality Checks
The package includes code quality tools:
# Code style (PSR-12)
vendor/bin/pint src tests
# Static analysis (PHPStan level 5)
vendor/bin/phpstan analyse src
All quality gates pass:
- ✅ Pint (0 style issues)
- ✅ PHPStan (0 errors)
- ✅ PHPUnit (18 passing tests)
Troubleshooting
"User not found" (401)
- Verify user exists:
User::where('email', 'test@company.com')->first() - Check column mapping:
GOOGLE_OAUTH_EMAIL_COLUMN - Verify email matches exactly (case-sensitive)
"Domain not allowed" (401)
- Check whitelist:
config('google-oauth.domain_whitelist') - Verify user's email domain is whitelisted
- To disable:
GOOGLE_OAUTH_DOMAIN_WHITELIST=(empty value)
"Token expired" (401)
- Provide
refresh_tokenin request body - Ensure Google OAuth credentials are correct
- Verify token is actually expired (check
expires_in)
"Service unavailable" (503)
- Check Google API status: https://status.cloud.google.com
- Verify
GOOGLE_OAUTH_CLIENT_IDandGOOGLE_OAUTH_CLIENT_SECRET - Check network connectivity to
https://www.googleapis.com
Tests Failing
- Ensure Composer dev dependencies installed:
composer install - Run with clean cache:
vendor/bin/phpunit --no-coverage - Check for mocking issues: Review
Http::fake()setup in test
API Reference
GoogleTokenValidator
Validates Google OAuth tokens and handles refresh logic.
use Impression\GooglePassportOAuth\Services\GoogleTokenValidator;
$validator = app(GoogleTokenValidator::class);
// Validate token (with optional refresh)
$tokenInfo = $validator->validate('access-token', 'refresh-token');
// Returns:
// [
// 'email' => 'user@example.com',
// 'name' => 'user@example.com',
// 'google_id' => 'google-123',
// 'expires_at' => Carbon instance,
// ]
UserSyncResolver
Finds users by email with domain whitelist enforcement.
use Impression\GooglePassportOAuth\Services\UserSyncResolver;
$resolver = app(UserSyncResolver::class);
// Resolve user (throws if not found or domain not allowed)
$user = $resolver->resolveByEmail('user@example.com');
Exceptions
All exceptions extend GoogleOAuthException:
use Impression\GooglePassportOAuth\Exceptions\{
InvalidTokenException,
TokenExpiredException,
RefreshFailedException,
GoogleApiException,
UserNotFoundByEmailException,
DomainNotAllowedException,
};
License
MIT
Contributing
Contributions welcome! Please ensure:
- Tests pass:
vendor/bin/phpunit - Code style passes:
vendor/bin/pint src tests - Static analysis passes:
vendor/bin/phpstan analyse src
Support
For issues and questions, please open an issue on GitHub or contact the Impression team.