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
- ✅ OAuth 2.1 MCP HTTP Registry Integration — Register organization OAuth metadata for Alexis discovery
- ✅ Fallback Authentication — Seamlessly fallback to parent app authentication when Google token unavailable
- ✅ Confirmation Workflow — Simple confirmation page for org OAuth connection registration
- ✅ 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
MCP OAuth 2.1 Registry Setup
This package supports OAuth 2.1 MCP HTTP Registry integration, allowing organizations to register protected resource metadata (PRM), authorization servers, and callback URLs for Alexis discovery. End users still sign in separately via their MCP connections (PKCE); org-level metadata is static and managed server-side.
Step 1: Publish MCP Migrations
php artisan vendor:publish --provider="Impression\GooglePassportOAuth\GoogleOAuthServiceProvider" --tag="google-oauth-migrations"
Step 2: Set MCP Environment Variables
Add to .env:
# MCP OAuth 2.1 Registry Configuration
MCP_OAUTH_PARENT_GUARD=web # Auth guard for parent app (default: web)
MCP_OAUTH_ISSUER_URL=https://your-app.com # Base URL identifying your OAuth provider
MCP_OAUTH_DISCOVERY_ENDPOINT=/oauth/mcp/discovery # MCP discovery endpoint (optional)
Step 3: Run Migrations
php artisan migrate
This creates the mcp_oauth_connections table for storing org-level OAuth metadata.
Step 4: Register MCP Routes (Optional)
If you want to expose MCP endpoints in your parent app, update routes/api.php or routes/web.php:
use Impression\GooglePassportOAuth\Http\Controllers\MCPTokenController;
use Impression\GooglePassportOAuth\Http\Controllers\MCPConfirmController;
use Impression\GooglePassportOAuth\Http\Controllers\MCPConnectionController;
use Impression\GooglePassportOAuth\Http\Middleware\EnsureOAuthAuthenticated;
Route::middleware(['throttle:60,1', EnsureOAuthAuthenticated::class])->group(function () {
// Exchange auth token for Passport token (Google or fallback parent app auth)
Route::post('/oauth/mcp/token', [MCPTokenController::class, 'exchange']);
// Show confirmation page with existing connections
Route::get('/oauth/mcp/confirm', [MCPConfirmController::class, 'show']);
// Register new MCP OAuth connection
Route::post('/oauth/mcp/connections', [MCPConnectionController::class, 'store']);
});
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' => env('GOOGLE_OAUTH_TOKEN_EXPIRES_IN', 31536000), // 1 year
// MCP OAuth 2.1 Registry configuration
'mcp_registry' => [
'parent_app_guard' => env('MCP_OAUTH_PARENT_GUARD', 'web'),
'discovery_endpoint' => env('MCP_OAUTH_DISCOVERY_ENDPOINT', '/oauth/mcp/discovery'),
'issuer_url' => env('MCP_OAUTH_ISSUER_URL'),
],
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
Google OAuth Token Exchange (Existing Flow)
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"
}
MCP OAuth 2.1 Registry Flow (New)
The MCP OAuth 2.1 flow allows organizations to register OAuth metadata for protected resource discovery. Organizations authenticate via either Google OAuth or fallback to parent app authentication, then register their OAuth 2.1 server details.
Authentication Flow
- Organization user is already authenticated in parent app (Laravel session)
- Organization user calls MCP endpoints without Google token (fallback to parent auth)
- Package middleware checks parent app session via configured guard
- Package issues Passport token for the authenticated user
- Organization visits confirmation page to review and register OAuth metadata
- Organization submits OAuth 2.1 metadata (PRM, authorization server URL, callback URL)
- Package stores metadata in
mcp_oauth_connectionstable - Alexis queries discovery endpoint to find registered OAuth configurations
MCP Token Exchange Endpoint
POST /oauth/mcp/token
This endpoint accepts both Google tokens and relies on parent app authentication.
Request (with Google token):
curl -X POST http://localhost:8000/oauth/mcp/token \
-H "Authorization: Bearer ya29.a0AfH6SMBx..." \
-H "Content-Type: application/json"
Request (fallback to parent app auth):
# User must be authenticated in parent app session
curl -X POST http://localhost:8000/oauth/mcp/token \
-H "Content-Type: application/json" \
-b "LARAVEL_SESSION=..." # Session cookie from parent app
Success Response (200 OK)
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 31536000
}
MCP Confirmation Endpoint
GET /oauth/mcp/confirm
Shows existing MCP OAuth connections and prompts for new metadata.
Request
curl http://localhost:8000/oauth/mcp/confirm \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
Success Response (200 OK)
{
"message": "Confirm MCP OAuth connection registration",
"issuer_url": "https://your-app.com",
"discovery_endpoint": "/oauth/mcp/discovery",
"existing_connections": [
{
"id": 1,
"name": "Acme Corp OAuth",
"authorization_server_url": "https://auth.acme.com/oauth/authorize",
"callback_url": "https://acme.com/oauth/callback",
"status": "active"
}
]
}
MCP Connection Registration Endpoint
POST /oauth/mcp/connections
Register a new OAuth 2.1 connection for the organization.
Request
curl -X POST http://localhost:8000/oauth/mcp/connections \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." \
-H "Content-Type: application/json" \
-d '{
"connection_name": "Acme Corp OAuth",
"authorization_server_url": "https://auth.acme.com/oauth/authorize",
"callback_url": "https://acme.com/oauth/callback",
"protected_resource_metadata": {
"scopes": ["openid", "profile", "email"],
"endpoints": {
"userinfo": "https://api.acme.com/userinfo",
"resource": "https://api.acme.com/resource"
}
},
"oauth_client_id": "acme-client-id",
"oauth_client_secret": "acme-client-secret"
}'
Parameters:
connection_name(required): Friendly name for this OAuth connectionauthorization_server_url(required): URL of the OAuth 2.1 authorization servercallback_url(required): Redirect URI for OAuth callbackprotected_resource_metadata(optional): JSON with scopes, endpoints, etc.oauth_client_id(optional): Static OAuth 2.1 client IDoauth_client_secret(optional): Static OAuth 2.1 client secret (encrypted server-side)
Success Response (201 Created)
{
"message": "MCP OAuth connection registered successfully",
"connection": {
"id": 1,
"name": "Acme Corp OAuth",
"authorization_server_url": "https://auth.acme.com/oauth/authorize",
"callback_url": "https://acme.com/oauth/callback",
"status": "active"
},
"issuer_url": "https://your-app.com",
"discovery_endpoint": "/oauth/mcp/discovery"
}
Error Responses
401 Unauthorized
{
"error": "unauthorized",
"message": "Unauthorized. Provide a valid Google token or authenticate with the parent application."
}
422 Unprocessable Entity — Validation failed
{
"message": "The connection_name field is required.",
"errors": {
"connection_name": ["The connection_name field is required."]
}
}
Integrating with Parent Laravel Application
To integrate this package as a sub-package in a larger Laravel application:
1. Install and Configure
composer require impression/laravel-google-passport-oauth
php artisan vendor:publish --provider="Impression\GooglePassportOAuth\GoogleOAuthServiceProvider" --tag="google-oauth-config"
php artisan vendor:publish --provider="Impression\GooglePassportOAuth\GoogleOAuthServiceProvider" --tag="google-oauth-migrations"
php artisan migrate
2. Set Environment Variables
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
MCP_OAUTH_PARENT_GUARD=web
MCP_OAUTH_ISSUER_URL=https://your-app.com
3. Register Routes in Parent App
In your parent app's routes/api.php:
Route::name('mcp.')->prefix('oauth')->group(function () {
// Google OAuth endpoint (existing)
Route::post('google-token', [\Impression\GooglePassportOAuth\Http\Controllers\GoogleTokenController::class, 'exchange'])
->middleware('throttle:60,1');
// MCP OAuth 2.1 endpoints (new)
Route::middleware(['throttle:60,1', \Impression\GooglePassportOAuth\Http\Middleware\EnsureOAuthAuthenticated::class])
->group(function () {
Route::post('mcp/token', [\Impression\GooglePassportOAuth\Http\Controllers\MCPTokenController::class, 'exchange']);
Route::get('mcp/confirm', [\Impression\GooglePassportOAuth\Http\Controllers\MCPConfirmController::class, 'show']);
Route::post('mcp/connections', [\Impression\GooglePassportOAuth\Http\Controllers\MCPConnectionController::class, 'store']);
});
});
4. Access MCP Models in Your App
use Impression\GooglePassportOAuth\Models\MCPOAuthConnection;
// Get all active connections for a user
$connections = MCPOAuthConnection::where('user_id', $user->id)->active()->get();
// Find a specific connection
$connection = MCPOAuthConnection::findOrFail($id);
// Update connection status
$connection->update(['status' => 'inactive']);
5. Share User Model Configuration
Ensure your parent app's User model is properly configured:
GOOGLE_OAUTH_USER_MODEL=App\Models\User
6. Trusted Proxies (If Behind Load Balancer)
If your parent app is behind a load balancer, configure config/trustedproxy.php to properly detect authenticated sessions.
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.