diff --git a/.env.example b/.env.example index 3ac86820..d2ad753b 100644 --- a/.env.example +++ b/.env.example @@ -103,11 +103,12 @@ LINKEDIN_CLIENT_ID= LINKEDIN_CLIENT_SECRET= LINKEDIN_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin/callback" LINKEDIN_PAGE_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin-page/callback" -# Optional: comma-separated extra OAuth scopes appended to the personal -# LinkedIn connect flow. Use this if your LinkedIn dev app has legacy or -# enterprise products approved — e.g. `r_basicprofile` re-enables -# vanityName lookup via /v2/me. Leave empty for the safe default set. -# LINKEDIN_EXTRA_SCOPES=r_basicprofile +# Optional: comma-separated OAuth scopes for each LinkedIn connect flow. +# Override these only if your LinkedIn dev app has legacy/enterprise products +# approved — e.g. add `r_basicprofile` to the personal set to re-enable the +# vanityName lookup via /v2/me. Leave unset to use the safe defaults. +# LINKEDIN_SCOPES="openid,profile,email,w_member_social" +# LINKEDIN_PAGE_SCOPES="openid,profile,email,w_organization_social,r_organization_social,rw_organization_admin,w_member_social" # X / Twitter (https://developer.twitter.com) X_CLIENT_ID= diff --git a/app/Http/Controllers/Auth/LinkedInController.php b/app/Http/Controllers/Auth/LinkedInController.php index 2a4b0d5e..a493d725 100644 --- a/app/Http/Controllers/Auth/LinkedInController.php +++ b/app/Http/Controllers/Auth/LinkedInController.php @@ -34,33 +34,7 @@ public function connect(Request $request): Response|RedirectResponse $this->authorize('manageAccounts', $workspace); - return $this->redirectToProvider($request, $this->driver, $this->resolveScopes()); - } - - /** - * Merge the default LinkedIn scopes with any extra scopes the operator - * configured via `LINKEDIN_EXTRA_SCOPES` — comma-separated, e.g. - * `r_basicprofile`. Useful when the connected LinkedIn dev app has legacy - * or enterprise products not covered by the default Sign-In + - * Share-on-LinkedIn pair. Both live under - * `config/trypost.php` → `platforms.linkedin`. - * - * @return array - */ - protected function resolveScopes(): array - { - /** @var array $scopes */ - $scopes = config('trypost.platforms.linkedin.scopes', []); - - $extra = (string) config('trypost.platforms.linkedin.extra_scopes', ''); - - if ($extra === '') { - return $scopes; - } - - $extraScopes = array_filter(array_map('trim', explode(',', $extra))); - - return array_values(array_unique([...$scopes, ...$extraScopes])); + return $this->redirectToProvider($request, $this->driver, config('trypost.platforms.linkedin.scopes')); } public function callback(Request $request): View diff --git a/app/Http/Controllers/Auth/LinkedInPageController.php b/app/Http/Controllers/Auth/LinkedInPageController.php index 03d58096..c60c6815 100644 --- a/app/Http/Controllers/Auth/LinkedInPageController.php +++ b/app/Http/Controllers/Auth/LinkedInPageController.php @@ -24,16 +24,6 @@ class LinkedInPageController extends SocialController protected SocialPlatform $platform = SocialPlatform::LinkedInPage; - protected array $scopes = [ - 'openid', - 'profile', - 'email', - 'w_organization_social', - 'r_organization_social', - 'rw_organization_admin', - 'w_member_social', - ]; - public function connect(Request $request): SymfonyResponse|RedirectResponse { $this->ensurePlatformEnabled(); @@ -55,7 +45,7 @@ public function connect(Request $request): SymfonyResponse|RedirectResponse return Inertia::location( Socialite::driver($this->driver) - ->scopes($this->scopes) + ->scopes(config('trypost.platforms.linkedin-page.scopes')) ->with([ 'redirect_uri' => config('services.linkedin-openid.redirect_page'), ]) @@ -80,7 +70,7 @@ public function callback(Request $request): View|RedirectResponse try { $socialUser = Socialite::driver($this->driver) - ->scopes($this->scopes) + ->scopes(config('trypost.platforms.linkedin-page.scopes')) ->with([ 'redirect_uri' => config('services.linkedin-openid.redirect_page'), ]) diff --git a/config/trypost.php b/config/trypost.php index 39882455..b5d35932 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -80,22 +80,23 @@ 'api' => env('LINKEDIN_API', 'https://api.linkedin.com'), // OAuth host is different from the data API (api.linkedin.com). 'oauth_api' => env('LINKEDIN_OAUTH_API', 'https://www.linkedin.com'), - // Default OAuth scopes requested on /connect/linkedin. Covers the - // two products auto-approved for new apps: Sign In with LinkedIn - // (openid, profile, email) + Share on LinkedIn (w_member_social). - // `r_basicprofile` is intentionally omitted — it's a 2018-deprecated - // legacy scope that new apps can't grant, and requesting it makes - // LinkedIn reject the whole authorize request. - 'scopes' => ['openid', 'profile', 'email', 'w_member_social'], - // Comma-separated extra scopes merged onto the defaults above, for - // apps with legacy/enterprise products approved — e.g. - // `LINKEDIN_EXTRA_SCOPES=r_basicprofile` to re-enable the vanityName - // lookup via /v2/me. - 'extra_scopes' => env('LINKEDIN_EXTRA_SCOPES'), + // Comma-separated OAuth scopes requested on /connect/linkedin. + // Default covers the two products auto-approved for new apps: Sign + // In with LinkedIn (openid, profile, email) + Share on LinkedIn + // (w_member_social). The 2018-deprecated `r_basicprofile` is + // intentionally absent — new apps can't grant it, and requesting it + // makes LinkedIn reject the whole authorize request. Override via + // LINKEDIN_SCOPES if your app has legacy/enterprise products + // approved (e.g. add `r_basicprofile` to re-enable the vanityName + // lookup via /v2/me). + 'scopes' => array_values(array_filter(array_map('trim', explode(',', (string) env('LINKEDIN_SCOPES', 'openid,profile,email,w_member_social'))))), ], 'linkedin-page' => [ 'enabled' => env('LINKEDIN_PAGE_ENABLED', true), 'api' => env('LINKEDIN_PAGE_API', 'https://api.linkedin.com'), + // Comma-separated OAuth scopes requested on the LinkedIn Company + // Page connect flow. Override via LINKEDIN_PAGE_SCOPES. + 'scopes' => array_values(array_filter(array_map('trim', explode(',', (string) env('LINKEDIN_PAGE_SCOPES', 'openid,profile,email,w_organization_social,r_organization_social,rw_organization_admin,w_member_social'))))), ], 'x' => [ 'enabled' => env('X_ENABLED', true), diff --git a/docker/.env.docker.example b/docker/.env.docker.example index e91739af..9395e6f8 100644 --- a/docker/.env.docker.example +++ b/docker/.env.docker.example @@ -90,11 +90,12 @@ LINKEDIN_CLIENT_ID= LINKEDIN_CLIENT_SECRET= LINKEDIN_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin/callback" LINKEDIN_PAGE_CLIENT_REDIRECT="${APP_URL}/accounts/linkedin-page/callback" -# Optional: comma-separated extra OAuth scopes appended to the personal -# LinkedIn connect flow. Use this if your LinkedIn dev app has legacy or -# enterprise products approved — e.g. `r_basicprofile` re-enables -# vanityName lookup via /v2/me. Leave empty for the safe default set. -# LINKEDIN_EXTRA_SCOPES=r_basicprofile +# Optional: comma-separated OAuth scopes for each LinkedIn connect flow. +# Override these only if your LinkedIn dev app has legacy/enterprise products +# approved — e.g. add `r_basicprofile` to the personal set to re-enable the +# vanityName lookup via /v2/me. Leave unset to use the safe defaults. +# LINKEDIN_SCOPES="openid,profile,email,w_member_social" +# LINKEDIN_PAGE_SCOPES="openid,profile,email,w_organization_social,r_organization_social,rw_organization_admin,w_member_social" X_CLIENT_ID= X_CLIENT_SECRET= diff --git a/tests/Feature/Social/LinkedInControllerTest.php b/tests/Feature/Social/LinkedInControllerTest.php index ef05246d..02ce1a50 100644 --- a/tests/Feature/Social/LinkedInControllerTest.php +++ b/tests/Feature/Social/LinkedInControllerTest.php @@ -71,22 +71,22 @@ return $captured; }; -test('linkedin connect requests the default scope set when LINKEDIN_EXTRA_SCOPES is unset', function () use ($captureConnectScopes) { - config(['trypost.platforms.linkedin.extra_scopes' => null]); +test('linkedin connect requests the default scope set', function () use ($captureConnectScopes) { + config(['trypost.platforms.linkedin.scopes' => ['openid', 'profile', 'email', 'w_member_social']]); expect($captureConnectScopes($this))->toEqualCanonicalizing([ 'openid', 'profile', 'email', 'w_member_social', ]); }); -test('linkedin connect appends LINKEDIN_EXTRA_SCOPES to the default scope set', function () use ($captureConnectScopes) { - // Backward-compatibility: ops who have legacy products approved on - // their LinkedIn app (e.g. r_basicprofile) opt back in via env. - config(['trypost.platforms.linkedin.extra_scopes' => 'r_basicprofile, r_emailaddress']); +test('linkedin connect requests the scopes configured via LINKEDIN_SCOPES', function () use ($captureConnectScopes) { + // Operators whose LinkedIn app has legacy/enterprise products approved + // override the default set via LINKEDIN_SCOPES (exploded into the config + // array), e.g. re-adding r_basicprofile to restore the vanityName lookup. + config(['trypost.platforms.linkedin.scopes' => ['openid', 'profile', 'email', 'w_member_social', 'r_basicprofile']]); expect($captureConnectScopes($this))->toEqualCanonicalizing([ - 'openid', 'profile', 'email', 'w_member_social', - 'r_basicprofile', 'r_emailaddress', + 'openid', 'profile', 'email', 'w_member_social', 'r_basicprofile', ]); });