Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Paypal Laravel Package

socialiteproviders/paypal

PayPal OAuth provider for Laravel Socialite. Adds PayPal login to your Laravel app via the Socialite Providers collection. Install with Composer, configure your client ID and secret, and authenticate users through PayPal in minutes.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Socialite driver, not a payments SDK. It answers one question: "who is this PayPal user?" If you need to create or capture orders, you want the PayPal Checkout REST API — this package will not help you.

1. Install the driver.

composer require socialiteproviders/paypal

This pulls socialiteproviders/manager alongside it and expects laravel/socialite to be present already.

2. Register the listener. Without this you get Driver [paypal] not supported.

Laravel ≤10 — app/Providers/EventServiceProvider.php:

protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        \SocialiteProviders\PayPal\PayPalExtendSocialite::class.'@handle',
    ],
];

Laravel 11+ — app/Providers/AppServiceProvider.php:

use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;

public function boot(): void
{
    Event::listen(function (SocialiteWasCalled $event) {
        $event->extendSocialite('paypal', \SocialiteProviders\PayPal\Provider::class);
    });
}

3. Credentials. Create a REST app in the PayPal developer dashboard, explicitly enable Log in with PayPal (it is a separate product toggle from Checkout), and set the return URL to your callback route.

// config/services.php
'paypal' => [
    'client_id'     => env('PAYPAL_CLIENT_ID'),
    'client_secret' => env('PAYPAL_CLIENT_SECRET'),
    'redirect'      => env('PAYPAL_REDIRECT_URI'),
],
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_REDIRECT_URI="${APP_URL}/auth/paypal/callback"

4. First use case.

Route::get('/auth/paypal/redirect', fn () => Socialite::driver('paypal')->redirect());

Route::get('/auth/paypal/callback', function () {
    $paypalUser = Socialite::driver('paypal')->user();

    $user = User::updateOrCreate(
        ['paypal_id' => $paypalUser->getId()],
        ['name' => $paypalUser->getName(), 'email' => $paypalUser->getEmail()],
    );

    Auth::login($user);

    return redirect('/dashboard');
});

Where to look first. The README for the exact listener snippet, then vendor/socialiteproviders/paypal/src/Provider.php — it is short and tells you precisely which endpoints, scopes, and response fields you get. If you need to understand getAuthUrl() / getTokenUrl() / mapUserToObject() semantics, fall back to Laravel\Socialite\Two\AbstractProvider.

Implementation Patterns

Controller over closure. Two methods, one class. You get dependency injection, structured logging, and a callback you can actually test.

public function redirect()
{
    return Socialite::driver('paypal')
        ->scopes(['openid', 'profile', 'email'])
        ->redirect();
}

public function callback(Request $request)
{
    try {
        $paypalUser = Socialite::driver('paypal')->user();
    } catch (\Laravel\Socialite\Two\InvalidStateException $e) {
        return redirect()->route('login')->withErrors('Login expired, please try again.');
    }

    // ...
}

Key on the PayPal payer ID, not email. $paypalUser->getId() is PayPal's stable identifier. Emails change and can be unverified — verified_account lives in the raw payload, so check $paypalUser->getRaw() before treating email as authoritative. The safe flow: match on paypal_id first, then offer to link to an existing local account by email rather than silently merging.

Persist the tokens, not just the profile. token, refreshToken, and expiresIn are all on the Socialite user object. If you will ever call PayPal APIs on the user's behalf, store them (encrypted cast) with an expiry.

$user->forceFill([
    'paypal_id'            => $paypalUser->getId(),
    'paypal_token'         => $paypalUser->token,
    'paypal_refresh_token' => $paypalUser->refreshToken,
    'paypal_expires_at'    => now()->addSeconds($paypalUser->expiresIn ?? 0),
])->save();

Refresh later with Socialite::driver('paypal')->refreshToken($user->paypal_refresh_token)->user(), but verify this against your target environment before building on it — the refresh leg hits the token endpoint with different constraints than the initial exchange.

Schema. Keep a single users table with a nullable paypal_id. If you expect more than one social provider, prefer a social_accounts table (provider, provider_user_id, tokens) over adding columns per provider. Either way, wrap the callback body in a DB::transaction() so a partial profile write doesn't leave a half-linked account.

Testing. Never hit PayPal in tests. Mock at the Socialite boundary with Socialite::shouldReceive('driver->user')->andReturn($fakeUser), or bind a fake driver into the container. For the token exchange itself, Http::fake() with a captured token + userinfo response gives you better regression coverage than mocks — it catches payload shape changes.

Extension point. Need a different endpoint set, extra scopes, or custom field mapping? Subclass SocialiteProviders\PayPal\Provider, override getAuthUrl() / getTokenUrl() / mapUserToObject(), and register your class in the extendSocialite() call. This is the supported escape hatch and is far cleaner than fighting config for environment differences.

Gotchas and Tips

  • Driver [paypal] not supported means the SocialiteWasCalled listener is missing, sits in an unregistered provider, or you never ran config:clear after editing services.php.
  • Redirect URI is byte-for-byte. Scheme, host, www., trailing slash — everything must match between PAYPAL_REDIRECT_URI and the PayPal app's return URL. Mismatches fail on PayPal's side before your app is reached, so Laravel logs stay empty. This is the single most common setup failure.
  • Scopes must be enabled in the dashboard. Requesting email when it isn't enabled returns invalid_scope from the token endpoint, not an auth error. Add the scope in code, add it in the dashboard.
  • Sandbox vs. live is the hidden trap. Confirm your host before trusting a test plan: dd(Socialite::driver('paypal')->redirect()->getTargetUrl()). Mixing a sandbox client ID against the live endpoint yields a confusing invalid_client.
  • Access tokens expire in hours. Any assumption that the login-time token is still valid on a later request is a latent bug. Store expiresIn, refresh, or re-authenticate.
  • Email may be absent or unverified. PayPal does not guarantee it; profile completeness depends on granted scopes and what the user has on their account. Make email nullable and never use it as a unique login key without verification.
  • InvalidStateException is normal traffic. Users open the login tab, wander off, come back to a stale session. Friendly redirect, not a 500.
  • Debugging the exchange. Catch the exception, log $e->getMessage() and Socialite::driver('paypal')->getTokenUrl(). Socialite folds the response body into the exception message, which is usually enough. For the raw profile, dump $paypalUser->getRaw() temporarily.
  • Secrets and config caching. client_secret belongs in .env only. Deploying with config:cache means a rotated secret won't take effect until you re-cache.
  • On 4.3.0 specifically: a minor bump with an auto-generated compare link and no documented breaking changes, deprecations, or features. Safe within a ^4.3 constraint, but skim the actual diff if you depend on a specific endpoint or user field — provider payload changes ship without changelog prose. Pin the constraint and let your callback tests, not the version number, be your regression signal.
  • The strategic risk worth flagging up front: "Log in with PayPal" is a distinct product from Checkout, and PayPal has communicated availability restrictions on new integrations in the past. Confirm its current status for your operating regions before building an auth dependency on it. A working payments integration does not imply a working login integration.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo