impression/laravel-google-passport-oauth

Google OAuth token exchange for Passport personal access tokens

Maintainers

Package info

github.com/JamesImpression/Laravel-Google-Passport-OAuth

pkg:composer/impression/laravel-google-passport-oauth

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-28 09:13 UTC

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

  1. External app (e.g., chatbot) obtains Google OAuth token from user
  2. External app sends token to your Laravel endpoint: POST /oauth/google-token
  3. Your package validates token with Google, finds user by email
  4. Your package issues Passport personal access token
  5. 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 token
  • refresh_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 database
  • domain_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

  1. 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
]);
  1. User authenticates via POST /oauth/google-token
  2. Package validates email matches (finds user)
  3. 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-token max 60 requests/minute per IP)
  • ✅ Use domain whitelist in production
  • ✅ Rotate Google OAuth credentials regularly

How It Works

  1. No token storage — Tokens validated in-memory, never persisted
  2. Server-side refresh — If token expired but refresh token provided, package refreshes silently
  3. Email-based lookup — User identity determined by email (not Google ID)
  4. Pre-provisioning only — Prevents account enumeration
  5. 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_token in 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_ID and GOOGLE_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.