Extending Laravel Socialite to Work with Google API Client

Recently I was working with Google API Client for PHP, for OAuth 2 Authentication I was using Laravel Socialite package, which works perfectly for basic authentication, it returns Access Token along with user email, name and other info, so where is the problem?

The problem arises when we try to use that access token with Google API Client for PHP, basically we can access Google API Client in one of two ways, OAuth and Non OAuth, when we just need to retrieve data we don't need OAuth and can use Non OAuth but for processing data such as uploading comments, videos and other stuff we need OAuth Authentication and for this we need to set access token using setAccessToken method.

Socialite google driver returns access token in simple string if we see access token after authentication, it looks something like this:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;

use Laravel\Socialite\Contracts\Factory as Socialite;
use Illuminate\Contracts\Auth\Guard as Auth;

class SocialLoginController extends Controller
{
    /**
     * @var Socialite
     */
    protected $socialite;
    /**
     * @var User
     */

    protected $auth;

    /**
     * @param Socialite $socialite
     * @param User $user
     * @param Auth $auth
     */

    protected $request;

    function __construct(Socialite $socialite, Auth $auth, Request $request)
    {
        $this->socialite = $socialite;
        $this->auth = $auth;
        $this->request = $request;
    }

    /**
     * @param $provider
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function login($provider)
    {
        return $this->execute($provider, $this->request->has('code'));
    }

    /**
     * @param $provider
     * @param $hasCode
     * @return \Illuminate\Http\RedirectResponse|\Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function execute($provider, $hasCode)
    {
        if( ! $hasCode)
        {
            return $this->getAuthorization($provider);

        }

        $user = $this->socialUser($provider);

      /***************************************************************
      /*
      /* dd($user->token); // will return something like this : "somerandomstring1232323123123"
      /*  
      /***************************************************************/ 
    }

    /**
     * @param $provider
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
     */
    public function getAuthorization($provider)
    {
        return $this->socialite->driver($provider)->redirect();
    }

    /**
     * @param $provider
     * @return \Laravel\Socialite\Contracts\User
     */
    public function socialUser($provider)
    {
        return $this->socialite->driver($provider)->user();
    }
}

as you can see $user->token returns string which contains access token, but in order to make it work we need the token in this format.

<?php

array:5 [▼
  "access_token" => "somerandomstring"
  "token_type" => "Bearer"
  "expires_in" => 3600
  "id_token" => "somelongrandomstring"
  "refresh_token" => "randomrefreshtokenstring"
]

As you can see, the access token we get from Laravel Socialite Google Provider is not enough and we need to extend it to make it work with Google API Client.

Let's Create a Custom Google Provider, to do that we first need to create folder where we are going to put our custom classes, for this instance we are going to create folder named Acme, inside that we are going to create another folder named socialite, inside socialite folder let's create GoogleCustomProvider.php file, it will contain our socialite provider.

app/Acme/GoogleCustomProvider.php

<?php

namespace App\Acme\Socialite;

use GuzzleHttp\ClientInterface;
use Laravel\Socialite\Two\AbstractProvider;
use Laravel\Socialite\Two\ProviderInterface;
use Laravel\Socialite\Two\User;

class GoogleCustomProvider extends AbstractProvider implements ProviderInterface {
    /**
     * The separating character for the requested scopes.
     *
     * @var string
     */
    protected $scopeSeparator = ' ';

    /**
     * The scopes being requested.
     *
     * @var array
     */
    protected $scopes = [
        'https://www.googleapis.com/auth/plus.me',
        'https://www.googleapis.com/auth/plus.login',
        'https://www.googleapis.com/auth/plus.profile.emails.read'
    ];

    /**
     * Get the access token for the given code.
     *
     * @param  string $code
     * @return string
     */
    public function getAccessToken( $code )
    {
        $postKey = (version_compare( ClientInterface::VERSION, '6' ) === 1) ? 'form_params' : 'body';


        $response = $this->getHttpClient()->post( $this->getTokenUrl(), [
            $postKey => $this->getTokenFields( $code ),
        ] );

//        this method is responsible to get only access token 
//        instead of getting full json token,
//        so this method is causing problem in original google provider class
//        so we disabled this method call and return full token instead of just access token
//        return $this->parseAccessToken();


        //return full token info instead of only access token token string
        return json_decode( $response->getBody(), true );
         
    }

    /**
     * {@inheritdoc}
     */
    protected function getTokenUrl()
    {
        return 'https://accounts.google.com/o/oauth2/token';
    }

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

    /**
     * {@inheritdoc}
     */
    protected function getAuthUrl( $state )
    {
        /*
         * added approval_promt=force && access_type=offline for getting refresh token in case session is not ended
         */
        $this->parameters["approval_prompt"] = "force";
        $this->parameters["access_type"] = "offline";
        return $this->buildAuthUrlFromBase( 'https://accounts.google.com/o/oauth2/auth', $state );
    }

    /**
     * {@inheritdoc}
     */
    protected function getUserByToken( $token )
    {
        $response = $this->getHttpClient()->get( 'https://www.googleapis.com/plus/v1/people/me?', [
            'query' => [
                'prettyPrint' => 'false',
            ],
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . $token['access_token'],//added ['access_token'] to get only token string
            ],
        ] );

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

    /**
     * {@inheritdoc}
     */
    protected function mapUserToObject( array $user )
    {
        return ( new User )->setRaw( $user )->map( [
            'id' => $user['id'], 'nickname' => array_get( $user, 'nickname' ), 'name' => $user['displayName'],
            'email' => $user['emails'][0]['value'], 'avatar' => array_get( $user, 'image' )['url'],
        ] );
    }
}

Only major change I did from original google provider is to comment the original return statement of getAccessToken which was parsing original token and just returning access token string, now I am just decoding JSON to PHP Array and returning it, this little change will do the trick.

Now All I need to to do is just register our custom access provider, for this we will use AppServiceProvider.php.

app/Providers/AppServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {

        $this->bootGoogleCustomSocialite();
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    private function bootGoogleCustomSocialite()
    {
        $socialite = $this->app->make('Laravel\Socialite\Contracts\Factory');
        $socialite->extend(
            'google', // extending default google with new full token functionality
            function ($app) use ($socialite) {
                $config = $app['config']['services.google'];
                return $socialite->buildProvider('App\Acme\Socialite\GoogleCustomProvider', $config);
            }
        );
    }
}

Note: Don't forget to add your google client id, client secret, redirect inside config/services.php

As you can see, I am overriding existing google provider, but you can change the name from google to something else if that's what you desire.

That's it, guys if you have any questions, ask us in the comment section below.

Something to say? Tell us in comment section.