/backend /PHP /tools

This article will guide you through the process of creating custom provider for OAuth2 authorization using php framework Laravel 5 and it's first-party package Socialite.

First of all let's say that social authorization is very popular and frankly speaking it's really handy tool. Surfing internet we can see a lot of sites and services which offer login with Facebook, Twitter, Google and other social networks. Quite frequently when building and maintaining web-sites a question of choosing appropriate tool/package/library arises. And there is a great amount of them.

If you're building your website in PHP using Laravel, probably you've noticed Socialite which provides OAuth / OAuth 2 authentication with Facebook, Twitter, Google, and GitHub.

The most famous social network which provides OAuth2 authentication in Russian segment of the internet is vk. But there is a lack of such connector (provider) in Socialite library. Actually it's not a hard problem, so let's build new VkProvider on top of Socialite's components.

Before we start building new provider please make sure you got the idea of OAuth2. Below is a cheat-sheet for the common workflow.

In order to use vk authorization for our app we need to create a standalone application. For more information please check this link. After you created the app you should pay attention to the settings in red boxes on image below.

In terms of Laravel we need to put these values under new section in services.php. Like this:

'vk' => [ 'client_id' => 'APPLICATION_ID', 'client_secret' => 'SECURE_KEY', 'redirect' => 'http://yoursite.com/callback_url' ] 1 2 3 4 5 'vk' = > [ 'client_id' = > 'APPLICATION_ID' , 'client_secret' = > 'SECURE_KEY' , 'redirect' = > 'http://yoursite.com/callback_url' ]

Then we need to add Socialite library to our project dependencies.

composer require "laravel/socialite": "~2.0" composer install 1 2 composer require "laravel/socialite" : "~2.0" composer install

In order to create new provider we need to extend AbstractProvider and implementProviderInterface according to Socialite's source code. In general UML-diagram will look like this:

From this diagram we can notice that we need to implement 4 methods in our FooProvider. After spending some time with VK api docs my VKProvider class was extended with more methods:

Vk's response contains not only access_token but also user's email (which will be cool to keep for our further needs), so we can override parseAccessToken() and user() in order to attach email to user's info. Class code:

<?php namespace App\Auth\Social\Two; use Laravel\Socialite\Two\AbstractProvider; use Laravel\Socialite\Two\ProviderInterface; use Laravel\Socialite\Two\User; class VkProvider extends AbstractProvider implements ProviderInterface { /** * VK API endpoint * * @var string */ protected $apiUrl = 'https://api.vk.com'; /** * VK API actual version * * @var string */ protected $version = '5.31'; /** * VK API Response language * On 'en' all data will be transliterated * * @var string */ protected $lang = 'ru'; /** * The scopes being requested * * @var array */ protected $scopes = ['email']; /** * User additional info fields * * By default returns: id, first_name, last_name * * @var array */ protected $infoFields = ['sex', 'photo_100', 'city', 'country', 'verified', 'site', 'nickname', 'screen_name']; /** * Get the authentication URL for the provider. * * @param string $state * @return string */ protected function getAuthUrl($state) { return $this->buildAuthUrlFromBase('https://oauth.vk.com/authorize', $state); } /** * Get the token URL for the provider. * * @return string */ protected function getTokenUrl() { return 'https://oauth.vk.com/access_token'; } /** * Get the raw user for the given access token. * * @param string $token * @return array */ protected function getUserByToken($token) { $access_token = $token['access_token']; $userInfoUrl = $this->apiUrl . '/method/users.get?access_token=' . $access_token . '&fields=' . implode(',', $this->infoFields) . '&lang=' . $this->lang . '&v=' . $this->version; $response = $this->getHttpClient()->get($userInfoUrl, [ 'headers' => [ 'Accept' => 'application/json', ], ]); $userData = json_decode($response->getBody(), true); $rawUser = reset($userData['response']); $rawUser['email'] = $token['email']; return $rawUser; } /** * Map the raw user array to a Socialite User instance. * * @param array $user * @return \Laravel\Socialite\User */ protected function mapUserToObject(array $user) { return (new User)->setRaw($user)->map([ 'id' => $user['id'], 'nickname' => $user['nickname'], 'name' => $user['first_name'] . ' ' . $user['last_name'], 'email' => isset($user['email']) ? $user['email'] : null, 'avatar' => $user['photo_100'] ]); } /** * Override default by adding API version * * @param string $state * @return array */ protected function getCodeFields($state) { $codeFields = parent::getCodeFields($state); $codeFields['v'] = $this->version; return $codeFields; } /** * Override in order to attach 'email' from requested permissions * * {@inheritdoc} */ public function user() { if ($this->hasInvalidState()) { throw new InvalidStateException; } $token = $this->getAccessToken($this->getCode()); $user = $this->mapUserToObject($this->getUserByToken($token)); return $user->setToken($token['access_token']); } /** * Return all decoded data in order to retrieve additional params like 'email' * * {@inheritdoc} */ protected function parseAccessToken($body) { return json_decode($body, true); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 <?php namespace App \ Auth \ Social \ Two ; use Laravel \ Socialite \ Two \ AbstractProvider ; use Laravel \ Socialite \ Two \ ProviderInterface ; use Laravel \ Socialite \ Two \ User ; class VkProvider extends AbstractProvider implements ProviderInterface { /** * VK API endpoint * * @var string */ protected $apiUrl = 'https://api.vk.com' ; /** * VK API actual version * * @var string */ protected $version = '5.31' ; /** * VK API Response language * On 'en' all data will be transliterated * * @var string */ protected $lang = 'ru' ; /** * The scopes being requested * * @var array */ protected $scopes = [ 'email' ] ; /** * User additional info fields * * By default returns: id, first_name, last_name * * @var array */ protected $infoFields = [ 'sex' , 'photo_100' , 'city' , 'country' , 'verified' , 'site' , 'nickname' , 'screen_name' ] ; /** * Get the authentication URL for the provider. * * @param string $state * @return string */ protected function getAuthUrl ( $state ) { return $this - > buildAuthUrlFromBase ( 'https://oauth.vk.com/authorize' , $state ) ; } /** * Get the token URL for the provider. * * @return string */ protected function getTokenUrl ( ) { return 'https://oauth.vk.com/access_token' ; } /** * Get the raw user for the given access token. * * @param string $token * @return array */ protected function getUserByToken ( $token ) { $access_token = $token [ 'access_token' ] ; $userInfoUrl = $this - > apiUrl . '/method/users.get?access_token=' . $access_token . '&fields=' . implode ( ',' , $this - > infoFields ) . '&lang=' . $this - > lang . '&v=' . $this - > version ; $response = $this - > getHttpClient ( ) - > get ( $userInfoUrl , [ 'headers' = > [ 'Accept' = > 'application/json' , ] , ] ) ; $userData = json_decode ( $response - > getBody ( ) , true ) ; $rawUser = reset ( $userData [ 'response' ] ) ; $rawUser [ 'email' ] = $token [ 'email' ] ; return $rawUser ; } /** * Map the raw user array to a Socialite User instance. * * @param array $user * @return \Laravel\Socialite\User */ protected function mapUserToObject ( array $user ) { return ( new User ) - > setRaw ( $user ) - > map ( [ 'id' = > $user [ 'id' ] , 'nickname' = > $user [ 'nickname' ] , 'name' = > $user [ 'first_name' ] . ' ' . $user [ 'last_name' ] , 'email' = > isset ( $user [ 'email' ] ) ? $user [ 'email' ] : null , 'avatar' = > $user [ 'photo_100' ] ] ) ; } /** * Override default by adding API version * * @param string $state * @return array */ protected function getCodeFields ( $state ) { $codeFields = parent : : getCodeFields ( $state ) ; $codeFields [ 'v' ] = $this - > version ; return $codeFields ; } /** * Override in order to attach 'email' from requested permissions * * {@inheritdoc} */ public function user ( ) { if ( $this - > hasInvalidState ( ) ) { throw new InvalidStateException ; } $token = $this - > getAccessToken ( $this - > getCode ( ) ) ; $user = $this - > mapUserToObject ( $this - > getUserByToken ( $token ) ) ; return $user - > setToken ( $token [ 'access_token' ] ) ; } /** * Return all decoded data in order to retrieve additional params like 'email' * * {@inheritdoc} */ protected function parseAccessToken ( $body ) { return json_decode ( $body , true ) ; } }

Now, we are almost ready to use this provider. But nevertheless one thing is left to do. The last thing we need to do is register SocialiteServiceProvider. According to the official docs we need to add it to config/app.php. This is the part where things get slightly complicated. Default service provider via dependency injection produces SocialiteManager class which doesn't know about our new VkProvider. That's why we will do the trick. After inspecting SocialiteManager code I realized that it was inherited from Laravel's Manager class which allows to add new drivers (providers) via extend() method.

So lets create new service provider called SocialitePlusServiceProvider where we will register new driver for vk.

<?php namespace App\Providers; use Laravel\Socialite\SocialiteManager; use Laravel\Socialite\SocialiteServiceProvider; class SocialitePlusServiceProvider extends SocialiteServiceProvider { public function register() { $this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) { $socialiteManager = new SocialiteManager($app); $socialiteManager->extend('vk', function() use ($socialiteManager) { $config = $this->app['config']['services.vk']; return $socialiteManager->buildProvider( 'App\Auth\Social\Two\VkProvider', $config ); }); return $socialiteManager; }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php namespace App \ Providers ; use Laravel \ Socialite \ SocialiteManager ; use Laravel \ Socialite \ SocialiteServiceProvider ; class SocialitePlusServiceProvider extends SocialiteServiceProvider { public function register ( ) { $this - > app - > bindShared ( 'Laravel\Socialite\Contracts\Factory' , function ( $app ) { $socialiteManager = new SocialiteManager ( $app ) ; $socialiteManager - > extend ( 'vk' , function ( ) use ( $socialiteManager ) { $config = $this - > app [ 'config' ] [ 'services.vk' ] ; return $socialiteManager - > buildProvider ( 'App\Auth\Social\Two\VkProvider' , $config ) ; } ) ; return $socialiteManager ; } ) ; } }

So that's it. Now we can add SocialitePlusServiceProvider in config/app.php and we are good to start using it in accordance with the documentation provided by Laravel. Files which were added to the project with appropriate folders:

In order to check how our new provider works let's create dummy example in routes.php:

Route::get('/login', function(\Illuminate\Http\Request $request) { if (!$request->has('code')) { return \Socialite::with('vk')->redirect(); } $user = \Socialite::with('vk')->user(); dd($user); }); 1 2 3 4 5 6 7 8 Route:: get ( '/login' , function ( \ Illuminate \ Http \ Request $request ) { if ( ! $request - > has ( 'code' ) ) { return \ Socialite:: with ( 'vk' ) - > redirect ( ) ; } $user = \ Socialite:: with ( 'vk' ) - > user ( ) ; dd ( $user ) ; } ) ;

If everything went fine you will receive similar dump:

So, you can use retrieved info in your app's authorization process. Also, on Laravel News I found that company DraperStudio has implemented 70 oauth(2) drivers for different social services.

However, they use Observer Pattern approach in order to register new providers. They also implemented Vkontakte provider which is pretty similar to introduced above.

Thanks for reading & I hope it was helpful for you.