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

Oauth2 Linkedin Laravel Package

league/oauth2-linkedin

A LinkedIn OAuth 2.0 provider for the League OAuth2 Client. It handles LinkedIn authorization redirects, access token exchange, and resource owner details, making LinkedIn login integration simple in PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

league/oauth2-linkedin is a provider plugin for league/oauth2-client — it does not implement OAuth by itself. Install it standalone and Composer pulls in the base client:

composer require league/oauth2-linkedin

You get one class you'll actually touch: League\OAuth2\Client\Provider\LinkedIn (plus LinkedInResourceOwner).

Before writing code, create an app in the LinkedIn Developer Portal, copy the Client ID / Client Secret, and register a redirect URI. LinkedIn matches redirect URIs exactly (scheme, host, trailing slash) and requires HTTPS except for http://localhost.

Where to look first:

  1. League\OAuth2\Client\Provider\LinkedIn — the endpoint URLs (/oauth/v2/authorization, /oauth/v2/accessToken), default scopes, and what getResourceOwner() fetches.
  2. The base league/oauth2-client docs — the flow (getAuthorizationUrl → getAccessToken → getResourceOwner) is identical across providers; only the LinkedIn class is package-specific.
  3. LinkedInResourceOwner — which accessors exist (getId(), getName(), getEmail(), getImageUrl(), toArray()).

First use case — a minimal callback flow:

use League\OAuth2\Client\Provider\LinkedIn;

$provider = new LinkedIn([
    'clientId'     => config('services.linkedin.client_id'),
    'clientSecret' => config('services.linkedin.client_secret'),
    'redirectUri'  => route('linkedin.callback'),
]);

// Step 1: redirect
$url = $provider->getAuthorizationUrl(['scope' => ['r_liteprofile', 'r_emailaddress']]);
session(['linkedin_state' => $provider->getState()]);
return redirect($url);

// Step 2: callback
abort_unless(request('state') === session('linkedin_state'), 403, 'Invalid state');
$token = $provider->getAccessToken('authorization_code', ['code' => request('code')]);
$user  = $provider->getResourceOwner($token);

$user->getId();       // LinkedIn member id
$user->getName();
$user->getEmail();
$user->toArray();     // raw payload — inspect this first when debugging

That is the entire happy path. Everything else is integration work.

Implementation Patterns

Two-route controller is the standard shape. One action builds the authorization URL and stores state; the other exchanges the code. Keep the provider construction in a small factory or resolve it from the container so config lives in config/services.php:

'linkedin' => [
    'client_id'     => env('LINKEDIN_CLIENT_ID'),
    'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
    'redirect'      => env('LINKEDIN_REDIRECT_URI'),
],

State belongs in the session, not the URL. Generate it with getAuthorizationUrl() (it sets $provider->getState() as a side effect), persist it, and compare on callback. In a stateless API you'll need to carry it in a signed cookie or a database row keyed to the user.

Persist the token, not the profile. Store linkedin_id on your users table for lookup, plus linkedin_access_token (cast encrypted) and linkedin_token_expires_at. Then resolve users with an upsert pattern:

$user = User::firstOrNew(['linkedin_id' => $owner->getId()]);
$user->fill([
    'name'  => $owner->getName(),
    'email' => $user->email ?? $owner->getEmail(),
]);
$user->linkedin_access_token = $token->getToken();
$user->linkedin_token_expires_at = now()->addSeconds($token->getExpires());
$user->save();

Use $token for later API calls. The provider gives you getAuthenticatedRequest() / getResponse() so the same token drives calls beyond login (posting to /v2/ugcPosts, reading /v2/me, etc.). Set the LinkedIn-specific headers yourself — most v2 endpoints need X-Restli-Protocol-Version: 2.0.0, and versioned endpoints need LinkedIn-Version: YYYYMM:

$request = $provider->getAuthenticatedRequest(
    'GET',
    'https://api.linkedin.com/v2/me',
    $token,
    ['headers' => [
        'X-Restli-Protocol-Version' => '2.0.0',
        'LinkedIn-Version'          => '202405',
    ]]
);
$response = $provider->getParsedResponse($request);

Scopes are per-request, not per-instance. Pass ['scope' => [...]] to getAuthorizationUrl(); the provider's default scopes are only a fallback. Request the minimum — r_liteprofile / r_emailaddress were the historical defaults.

Test without hitting LinkedIn. The provider accepts a Guzzle client via constructor options, and oauth2-client ships mock responses. Use MockHandler in feature tests so getAccessToken and getResourceOwner return fixtures; assert your user-resolution logic, not LinkedIn.

Treat it as a login library, not a social SDK. For richer LinkedIn actions (sharing, org pages), keep using this package for the token handshake and call the REST API with Http::withToken(...) or the authenticated-request helpers — don't expect the provider to wrap every endpoint.

Gotchas and Tips

  • This package predates LinkedIn's 2023 OpenID Connect migration — this is the big one. New LinkedIn apps are issued openid, profile, email scopes and must use https://api.linkedin.com/v2/userinfo. The legacy r_liteprofile / r_emailaddress scopes are deprecated for new apps, and getResourceOwner() will 403 if it still targets /v2/me. Workaround: use the provider purely for the token exchange, then call /v2/userinfo yourself with the access token, or subclass LinkedIn and override getResourceOwnerDetailsUrl() / getResourceOwner(). Verify against your app's actual approved scopes before shipping.
  • Email is often not where you expect it. Even with r_emailaddress, LinkedIn historically returned email from a separate /v2/emailAddress call, not the profile payload. Always inspect $owner->toArray() before trusting getEmail(). With OIDC email comes from userinfo.
  • No refresh tokens. Standard LinkedIn OAuth issues a ~60-day access token and no refresh grant. Your "remember me" logic must handle expiry by re-running the authorization flow; don't build a refresh path that will never fire.
  • Redirect URI mismatch is the #1 login failure. LinkedIn rejects with invalid_redirect_uri on tiny differences. Configure one URI per environment in the portal and keep env() values in sync — trailing slashes and http/https are common culprits.
  • State validation is mandatory. Skipping the state comparison is a CSRF hole. guzzlehttp will happily complete the exchange without it; nothing in the package enforces it for you.
  • Catch IdentityProviderException. Token exchange and resource-owner calls throw League\OAuth2\Client\Provider\Exception\IdentityProviderException. Its getResponseBody() carries LinkedIn's real error (invalid_request, invalid_scope, etc.) — log that, not just the message. Users denying consent surface as access_denied in the query string before you ever reach the provider.
  • Scope approval is asynchronous. Scopes like rw_organization_admin require LinkedIn's Marketing Developer Platform review. Requesting an unapproved scope fails at the authorization step, not at token exchange, so it looks like a config bug.
  • PHP 8 edge cases. The 2020-era release targets PHP 7.x. Pin league/oauth2-client to a ^2.7-era version compatible with your PHP, and run your test suite on PHP 8.2+ to catch deprecations before production.
  • Never log or expose tokens. $token->getToken() in a log line or an Inertia prop is a 60-day credential. Encrypt at rest and exclude from toArray() output on your user model.
  • Extension points are clean. Subclass LinkedIn for custom endpoints, inject a configured Guzzle client (timeouts, retries) via constructor options, and override getBaseAuthorizationUrl() / getDefaultScopes() for regional or OIDC variants rather than forking the package.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core