socialiteproviders/linkedin
Community-maintained LinkedIn OAuth provider for Laravel Socialite. It adds LinkedIn sign-in and user profile retrieval to your Laravel application through a simple Composer install, client ID and secret configuration, and the standard Socialite driver API you already know.
This package is a community provider for Laravel Socialite that adds a linkedin driver. It is a subtree split of SocialiteProviders/Providers, so the repo you see is generated — treat the monorepo as the upstream source of truth, but composer require pulls the split package directly.
1. Install
composer require socialiteproviders/linkedin
This pulls in socialiteproviders/manager, which does the actual driver registration.
2. Register the Socialite listener
Laravel 11+ (bootstrap/app.php / AppServiceProvider::boot):
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
Event::listen(SocialiteWasCalled::class, function (SocialiteWasCalled $event) {
$event->extendSocialite('linkedin', \SocialiteProviders\LinkedIn\LinkedInExtendSocialite::class);
});
Laravel ≤10 (app/Providers/EventServiceProvider.php):
protected $listen = [
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\LinkedIn\LinkedInExtendSocialite::class.'@handle',
],
];
3. Configure credentials in config/services.php:
'linkedin' => [
'client_id' => env('LINKEDIN_CLIENT_ID'),
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
'redirect' => env('LINKEDIN_REDIRECT_URI'),
],
4. First use case — a "Sign in with LinkedIn" button:
Route::get('/auth/linkedin/redirect', fn () => Socialite::driver('linkedin')->redirect());
Route::get('/auth/linkedin/callback', function () {
$linkedinUser = Socialite::driver('linkedin')->user();
// $linkedinUser->getId(), ->getName(), ->getEmail(), ->getAvatar()
});
Where to look first in the installed vendor directory:
src/LinkedInExtendSocialite.php — the one-liner that registers the driver.src/LinkedInProvider.php — scopes, endpoints, scope defaults, and the mapUserToObject implementation. Read this before debugging.composer.json — confirms the Socialite version constraint (usually ^5.x) and PHP floor (^8.x).Controller/service class split. Keep OAuth redirect plumbing out of controllers that also do business logic. A small LinkedInAuthService wrapping Socialite::driver('linkedin') makes it far easier to mock and reuse across web + API entry points.
Standard login flow with account linking.
class LinkedInCallbackController
{
public function __invoke(Request $request, LinkedInAuthService $linkedIn)
{
$remote = $linkedIn->user(); // wraps Socialite::driver('linkedin')->user()
$user = User::updateOrCreate(
['linkedin_id' => $remote->getId()],
[
'name' => $remote->getName() ?? 'LinkedIn User',
'email' => $remote->getEmail(), // may be null — see gotchas
]
);
Auth::login($user, remember: true);
return redirect()->intended('/dashboard');
}
}
Guard against takeover: only match on linkedin_id, and reconcile by email explicitly with user confirmation — never User::firstOrCreate(['email' => $remote->getEmail()]) blindly.
Stateless / API + SPA usage. Socialite stores state in the session. For token-based or cross-domain flows, use stateless mode and validate state yourself:
Socialite::driver('linkedin')->stateless()->redirect();
// callback
$user = Socialite::driver('linkedin')->stateless()->user();
Custom scopes. Pass scopes per request rather than editing the provider:
Socialite::driver('linkedin')
->scopes(['openid', 'profile', 'email', 'w_member_social'])
->redirect();
Scopes must be enabled on the LinkedIn app (Products tab) or LinkedIn rejects the authorize request before it ever reaches your callback.
Testing. Socialite has no built-in fake for third-party providers, so wrap the driver and bind an interface in the container:
$this->mock(LinkedInAuthService::class, function ($mock) {
$mock->shouldReceive('user')->andReturn(
(new \Laravel\Socialite\Two\User)->map([
'id' => 'abc123', 'name' => 'Test', 'email' => '[email protected]',
])
);
});
Refresh / re-auth. LinkedIn tokens are short-lived; for API calls use Socialite::driver('linkedin')->userFromToken($token) where supported, or store refresh_token and hit the token endpoint directly. Don't assume the token from user() persists.
Integration tip — pair with the OIDC driver. Core Socialite ships linkedin-openid. If you only need login (no legacy v2 API fields), prefer linkedin-openid and drop this package; use socialiteproviders/linkedin when you need the SocialiteProviders manager integration, custom scopes, or a unified driver registry across many providers.
Event-driven registration scales cleanly. In apps with many SocialiteProviders packages, register a single listener that loops a config array of provider => handler classes rather than a pile of individual listeners.
Redirect URI is exact-match, HTTPS-only. LinkedIn compares the redirect_uri character-for-character. Trailing slash, http vs https, www vs apex, and port mismatches all cause redirect_uri does not match. Local dev typically needs https://127.0.0.1:8000/... or a tunnel (ngrok/Valet share) — plain http://localhost often fails.
"Driver [linkedin] is not supported." Almost always the SocialiteWasCalled listener is missing, wrong, or not booted (config/event cache). Run php artisan event:cache after adding it, and verify with php artisan event:list | grep Socialite.
Email can be null. Even with the email scope, LinkedIn can omit it (users can hide email). Always handle null in updateOrCreate and in your "verified email" assumptions — don't mark it verified just because LinkedIn returned it.
Scopes are the #1 source of silent failures. LinkedIn's current product is Sign In with LinkedIn using OpenID Connect (openid profile email). Legacy r_liteprofile / r_emailaddress scopes are deprecated and error for apps created after the cutoff. Check LinkedInProvider::getScopes() in src/ and override per driver:
Socialite::driver('linkedin')->scopes(['openid', 'profile', 'email'])->redirect();
Avatar URLs expire. getAvatar() returns a signed CDN URL with a lifespan; persist it for display or download it to your own storage during signup, don't cache the URL forever.
InvalidStateException / session issues. Symptoms: state mismatch on callback. Causes: stateless flow used but you called the stateful redirect(), cross-subdomain cookie scope, SESSION_DRIVER=array, or the user opened multiple login tabs. Fix by matching stateful/stateless consistently and setting SESSION_DOMAIN correctly.
API versioning header. LinkedIn's REST API requires a Linkedin-Version header (e.g. 202411) on non-OAuth calls. If you extend the provider to call LinkedIn endpoints, set it — an outdated version yields 426 Upgrade Required.
Extending the provider. To customize fields or endpoints, subclass:
class MyLinkedInProvider extends \SocialiteProviders\LinkedIn\LinkedInProvider
{
protected function mapUserToObject(array $user): \Laravel\Socialite\Two\User { /* ... */ }
public function getAuthUrl($state): string { /* ... */ }
}
Then register MyLinkedInProvider in the SocialiteWasCalled listener instead of the package's class. Prefer this over editing vendor files — subtree splits overwrite on composer update.
Config caching bites. php artisan config:clear after changing services.php, and remember cached config ignores .env mutations in production-like setups. A "wrong client_id" bug is frequently a stale config cache.
Subtree split lag. The standalone README can trail the monorepo. If a scope or endpoint looks wrong, check SocialiteProviders/Providers on GitHub and pin the version in composer.json when relying on recent changes (last release 2024-11-27 — verify it supports the current LinkedIn OIDC behavior before assuming).
How can I help you explore Laravel packages today?