laravel / socialite

Laravel wrapper around OAuth 1 & OAuth 2 libraries.
https://laravel.com/docs/socialite
MIT License
5.58k stars 940 forks source link

Issue with oAuth 2.0 LinkedIn Upgrade API Web Service #309

Closed noeurphireak closed 5 years ago

noeurphireak commented 5 years ago

I got this error when I try to login with my LinkedIn Account laravel/socialite": "^3.1"

and here is script $userSocial = Socialite::driver('linkedin')->user();

here is the developer update from linkedin (https://engineering.linkedin.com/blog/2018/12/developer-program-updates)

Client error: GET https://api.linkedin.com/v1/people/~:(id,first-name,last-name,formatted-name,email-address,headline,location,industry,public-profile-url,picture-url,picture-urls::(original)) resulted in a 410 Gone response: { "errorCode": 0, "message": "This resource is no longer available under v1 APIs", "requestId": "3WIBWBXOPW", "s (truncated...)

How to fix this issue ?

Thank you

riasvdv commented 5 years ago

+1 All social logins with LinkedIn are now broken

driesvints commented 5 years ago

I've been working on a solution and believe I'm nearly there but at the moment I'm bumping into the following error when performing a request to the https://api.linkedin.com/v2/me endpoint.

Client error: GET https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams)) resulted in a 403 Forbidden response: {"serviceErrorCode":100,"message":"Not enough permissions to access: GET /me","status":403}

It seems I have no permissions to perform an API request to the endpoint even though I correctly authenticated and have added the correct scopes. Some suggest you need permission from LinkedIn itself in order to perform the API request but that seems a tad old and it would be surprising if that was still the case: https://stackoverflow.com/questions/46960458/any-queries-to-the-api-linkedin-com-v2-return-not-enough-permissions-to-access

Also see my answer on a StackOverflow issue here: https://stackoverflow.com/questions/50363237/not-enough-permissions-to-access-me-get/50943840#comment94493533_50943840

Here's what I have so far:

<?php

namespace Laravel\Socialite\Two;

use Illuminate\Support\Arr;

class LinkedInProvider extends AbstractProvider implements ProviderInterface
{
    /**
     * The scopes being requested.
     *
     * @var array
     */
    protected $scopes = ['r_basicprofile', 'r_emailaddress'];

    /**
     * The separating character for the requested scopes.
     *
     * @var string
     */
    protected $scopeSeparator = ' ';

    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state);
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return 'https://www.linkedin.com/oauth/v2/accessToken';
    }

    /**
     * Get the POST fields for the token request.
     *
     * @param  string  $code
     * @return array
     */
    protected function getTokenFields($code)
    {
        return parent::getTokenFields($code) + ['grant_type' => 'authorization_code'];
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken($token)
    {
        $basicProfile = $this->getBasicProfile($token);
        $emailAddress = $this->getEmailAddress($token);

        return array_merge($basicProfile, $emailAddress);
    }

    /**
     * Get the basic profile fields for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getBasicProfile($token)
    {
        $url = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true);
    }

    /**
     * Get the email address for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getEmailAddress($token)
    {
        $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true)['handle~'];
    }

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject(array $user)
    {
        $name = Arr::get($user, 'firstName.localized.en_US').' '.Arr::get($user, 'lastName.localized.en_US');

        return (new User)->setRaw($user)->map([
            'id' => $user['id'],
            'nickname' => null,
            'name' => $name,
            'email' => Arr::get($user, 'emailAddress'),
            // 'avatar' => Arr::get($user, 'pictureUrl'),
            // 'avatar_original' => Arr::get($user, 'pictureUrls.values.0'),
        ]);
    }
}

I was trying to do a request to see what the output of the profilePicture field would be because at the moment I have no clue how the Asset Playable Streams work: https://docs.microsoft.com/en-us/linkedin/shared/references/v2/digital-media-asset#asset-playable-streams-table

This all looks super complex and far fetched if you ask me. Any help is greatly appreciated.

tomsisk commented 5 years ago

Our application does not have a problem right now (not sure if it did earlier). The linked update says v1 to be retired in March, so it sounds like they may have just messed up by breaking this earlier?

@driesvints Try using r_liteprofile instead of r_basicprofile. Their v2 API mentions nothing about r_basicprofile when it comes to Sign In with LinkedIn:

When requesting an authorization code in Step 2 of the OAuth 2.0 Guide, make sure to request the r_liteprofile and/or r_emailaddress scopes!

https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin

driesvints commented 5 years ago

@tomsisk thanks. That wasn't really obvious at the beginning because with the r_basicprofile you're suppose to get much more fields. I've updated it to that scope and it now works 👍

I'll send in a PR in a bit.

driesvints commented 5 years ago

Here it is: https://github.com/laravel/socialite/pull/310

driesvints commented 5 years ago

Merged and should be fixed with the next major release.

noeurphireak commented 5 years ago

Okay thank @driesvints and everyone for your time answer my question. I will try that

chrisGeonet commented 5 years ago

Is it possible to update this in the v3 branch? My site can't be updated past Laravel 5.4/

driesvints commented 5 years ago

@chrisGeonet no since v3.0 uses the old implementation and it'll break for people who are still on it. You'll have to upgrade to 4.0.

chrisGeonet commented 5 years ago

Sorry, I don't understand your reasoning. Are you saying that if the V3 code is updated, it will break existing V3 implementations? It will break anyway when LinkedIn discontinue the V1 API on March 1st.

The Google authentication was updated on V3 when the GooglePlus API was discontinued.

Upgrading isn't always an option for older sites.

driesvints commented 5 years ago

@chrisGeonet if we replace the current code on v3 with the new LinkedIn integration, everyone who uses the current old LinkedIn integration will see their apps break. If you want to use the new LinkedIn integration you'll need to update to v4.

Krato commented 5 years ago

You can create a custom Provider for Socialite and use @driesvints code. Works fine. Thanks mate!

jeremykenedy commented 5 years ago

@Krato What does that provider look like and how did you implement it?

Did you call it in EventServiceProvider.php?

This is what I have:

<?php

namespace App\Providers;

use Illuminate\Support\Arr;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;

class LinkedInServiceProvider extends AbstractProvider implements ProviderInterface
{
    /**
     * The scopes being requested.
     *
     * @var array
     */
    protected $scopes = ['r_basicprofile', 'r_emailaddress'];

    /**
     * The separating character for the requested scopes.
     *
     * @var string
     */
    protected $scopeSeparator = ' ';

    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state);
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return 'https://www.linkedin.com/oauth/v2/accessToken';
    }

    /**
     * Get the POST fields for the token request.
     *
     * @param  string  $code
     * @return array
     */
    protected function getTokenFields($code)
    {
        return parent::getTokenFields($code) + ['grant_type' => 'authorization_code'];
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken($token)
    {
        $basicProfile = $this->getBasicProfile($token);
        $emailAddress = $this->getEmailAddress($token);

        return array_merge($basicProfile, $emailAddress);
    }

    /**
     * Get the basic profile fields for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getBasicProfile($token)
    {
        $url = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true);
    }

    /**
     * Get the email address for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getEmailAddress($token)
    {
        $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization' => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true)['handle~'];
    }

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject(array $user)
    {
        $name = Arr::get($user, 'firstName.localized.en_US').' '.Arr::get($user, 'lastName.localized.en_US');

        return (new User)->setRaw($user)->map([
            'id' => $user['id'],
            'nickname' => null,
            'name' => $name,
            'email' => Arr::get($user, 'emailAddress'),
            // 'avatar' => Arr::get($user, 'pictureUrl'),
            // 'avatar_original' => Arr::get($user, 'pictureUrls.values.0'),
        ]);
    }
}

Here is my EventServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;

class EventServiceProvider extends ServiceProvider
{
    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        'App\Events\SomeEvent' => [
            'App\Listeners\EventListener',
        ],
        \SocialiteProviders\Manager\SocialiteWasCalled::class => [
            'SocialiteProviders\YouTube\YouTubeExtendSocialite@handle',
            'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
            'SocialiteProviders\Instagram\InstagramExtendSocialite@handle',
            'App\Providers\LinkedInServiceProvider',
            'SocialiteProviders\ThirtySevenSignals\ThirtySevenSignalsExtendSocialite@handle',
        ],
    ];

    /**
     * Register any events for your application.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();

        //
    }
}

And I get the following error:

Unresolvable dependency resolving [Parameter #1 [ <required> $clientId ]] in class Laravel\Socialite\Two\AbstractProvider
Krato commented 5 years ago

@jeremykenedy I use a custom social provider and a custom controller. Check this guide: https://www.laragle.com/2017/03/step-by-step-laravel-social-login.html

And use this class using your own namespace:

<?php

namespace App\Social;

use Illuminate\Support\Arr;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

class LinkedinV2ServiceProvider extends AbstractProvider implements ProviderInterface
{
    /**
     * The scopes being requested.
     *
     * @var array
     */
    protected $scopes = ['r_liteprofile', 'r_emailaddress'];

    /**
     * The separating character for the requested scopes.
     *
     * @var string
     */
    protected $scopeSeparator = ' ';

    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl($state)
    {
        return $this->buildAuthUrlFromBase('https://www.linkedin.com/oauth/v2/authorization', $state);
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return 'https://www.linkedin.com/oauth/v2/accessToken';
    }

    /**
     * Get the POST fields for the token request.
     *
     * @param  string  $code
     * @return array
     */
    protected function getTokenFields($code)
    {
        return parent::getTokenFields($code) + ['grant_type' => 'authorization_code'];
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken($token)
    {
        $basicProfile = $this->getBasicProfile($token);
        $emailAddress = $this->getEmailAddress($token);

        return array_merge($basicProfile, $emailAddress);
    }

    /**
     * Get the basic profile fields for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getBasicProfile($token)
    {
        $url = 'https://api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization'             => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true);
    }

    /**
     * Get the email address for the user.
     *
     * @param  string  $token
     * @return array
     */
    protected function getEmailAddress($token)
    {
        $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))';

        $response = $this->getHttpClient()->get($url, [
            'headers' => [
                'Authorization'             => 'Bearer '.$token,
                'X-RestLi-Protocol-Version' => '2.0.0',
            ],
        ]);

        return json_decode($response->getBody(), true)['elements'][0]['handle~'];
    }

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject(array $user)
    {
        $name = Arr::get($user, 'firstName.localized.en_US').' '.Arr::get($user, 'lastName.localized.en_US');
        $images = Arr::get($user, 'profilePicture.displayImage~.elements');
        $avatar = Arr::first(Arr::where($images, function ($image) {
            return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 100;
        }));
        $originalAvatar = Arr::first(Arr::where($images, function ($image) {
            return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 800;
        }));

        return (new User())->setRaw($user)->map([
            'id'              => $user['id'],
            'nickname'        => null,
            'name'            => $name,
            'email'           => Arr::get($user, 'emailAddress'),
            'avatar'          => Arr::get($avatar, 'identifiers.0.identifier'),
            'avatar_original' => Arr::get($originalAvatar, 'identifiers.0.identifier'),
        ]);
    }
}

And extend socialite providers in your AppServiceProvider or in your own provider to override LinkedIn provider

$socialite = $this->app->make('Laravel\Socialite\Contracts\Factory');

$socialite->extend(
    'linkedinv2',
    function ($app) use ($socialite) {
        $config = $app['config']['services.linkedin'];

        return $socialite->buildProvider(LinkedinV2ServiceProvider::class, $config);
    }
);

Regards!

webexpert4rv commented 5 years ago

image @Krato I follow the same but getting above error. Can you please help me for the same. I'm using laravel 5.1

RDZU commented 5 years ago

Hi I had the same problem on Socialite 3.3

I solved it, I changed the file LinkedInProvider.php with the source code below

/ vendor/laravel/socialite/src/two/LinkedInProvider /

`<?php

namespace Laravel\Socialite\Two;

use Illuminate\Support\Arr;

class LinkedInProvider extends AbstractProvider implements ProviderInterface { /**

/**

/**

/**

/**

/**

/**

/**

/**

}`

bhumikaZen commented 1 year ago

Hi I had the same problem on Socialite 3.3

I solved it, I changed the file LinkedInProvider.php with the source code below

/ vendor/laravel/socialite/src/two/LinkedInProvider /

`<?php

namespace Laravel\Socialite\Two;

use Illuminate\Support\Arr;

class LinkedInProvider extends AbstractProvider implements ProviderInterface { /* The scopes being requested. @var array */ protected $scopes = ['r_liteprofile', 'r_emailaddress'];

/**

  • The separating character for the requested scopes.
  • @var string */ protected $scopeSeparator = ' ';

/**

/**

/**

  • Get the POST fields for the token request.
  • @param string $code
  • @return array */ protected function getTokenFields($code) { return parent::getTokenFields($code) + ['grant_type' => 'authorization_code']; }

/**

  • {@inheritdoc} */ protected function getUserByToken($token) { $basicProfile = $this->getBasicProfile($token); $emailAddress = $this->getEmailAddress($token); return array_merge($basicProfile, $emailAddress); }

/**

/**

  • Get the email address for the user.
  • @param string $token
  • @return array / protected function getEmailAddress($token) { $url = 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements(handle~))'; $response = $this->getHttpClient()->get($url, [ 'headers' => [ 'Authorization' => 'Bearer '.$token, 'X-RestLi-Protocol-Version' => '2.0.0', ], ]); return (array) Arr::get((array) json_decode($response->getBody(), true), 'elements.0.handle~'); }

/**

  • {@inheritdoc} */ protected function mapUserToObject(array $user) { $preferredLocale = Arr::get($user, 'firstName.preferredLocale.language').'_'.Arr::get($user, 'firstName.preferredLocale.country'); $firstName = Arr::get($user, 'firstName.localized.'.$preferredLocale); $lastName = Arr::get($user, 'lastName.localized.'.$preferredLocale); $images = (array) Arr::get($user, 'profilePicture.displayImage~.elements', []); $avatar = Arr::first(Arr::where($images, function ($image) { return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 100; })); $originalAvatar = Arr::first(Arr::where($images, function ($image) { return $image['data']['com.linkedin.digitalmedia.mediaartifact.StillImage']['storageSize']['width'] === 800; })); return (new User)->setRaw($user)->map([ 'id' => $user['id'], 'nickname' => null, 'name' => $firstName.' '.$lastName, 'first_name' => $firstName, 'last_name' => $lastName, 'email' => Arr::get($user, 'emailAddress'), 'avatar' => Arr::get($avatar, 'identifiers.0.identifier'), 'avatar_original' => Arr::get($originalAvatar, 'identifiers.0.identifier'), ]); }

}`

This works for me.Thanks for the answer

mbh00 commented 8 months ago

I ran into this problem also. i didn't figure out guys already updated the provider. there are two linkedin providers now I used 'linkedin-openid' instead of 'linkedin' Socialite::driver('linkedin-openid') also make sure to add an entry for linkedin-openid in the service file config