impression / laravel-google-passport-oauth
Google OAuth token exchange for Passport personal access tokens
Package info
github.com/JamesImpression/Laravel-Google-Passport-OAuth
pkg:composer/impression/laravel-google-passport-oauth
Requires
- php: ^8.1
- laravel/framework: ^11.0 || ^12.0
- laravel/passport: ^12.0 || ^13.0
- laravel/socialite: ^5.0
Requires (Dev)
- laravel/pint: ^1.29
- orchestra/testbench: ^9.0 || ^10.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
This package is not auto-updated.
Last update: 2026-04-28 12:30:06 UTC
README
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.