Laravel: Создание драйвера для Laravel Socialite

Источник: «Implement a custom driver for Laravel Socialite»
Laravel Socialite — официальный пакет Laravel для аутентификации у провайдеров OAuth. Он поддерживает аутентификацию с помощью Facebook, Twitter, LinkedIn, Google, GitHub и Bitbucket. Но что, если вы хотите использовать другой драйвер?

В нашем случае мы хотим использовать AWS Cognito в качестве поставщика аутентификации. AWS Cognito позволяет выполнять аутентификацию с использованием различных поставщиков и хранить централизованные пользовательские данные, которые можно использовать для различных приложений.

Итак, первым делом нужно установить Laravel Socialite, вот так:

composer require laravel/socialite

Теперь мы создадим класс CognitoProvider, который расширяется из \Socialite\Two\AbstractProvider. Нам нужно реализовать следующие методы, чтобы драйвер работал так, как ожидается:

// ...
use Laravel\Socialite\Two\AbstractProvider;

class SocialiteCognitoProvider extends AbstractProvider
{

protected function getAuthUrl($state)
{
// TODO: Реализовать метод getAuthUrl().
}

protected function getTokenUrl()
{
// TODO: Реализовать метод getTokenUrl().
}

protected function getUserByToken($token)
{
// TODO: Реализовать метод getUserByToken().
}

protected function mapUserToObject(array $user)
{
// TODO: Реализовать метод mapUserToObject().
}
}

Из документации Laravel Socialite следует, что нам нужно создать маршрут redirect, который вызывает метод redirect() из выбранного драйвера, например:

use Laravel\Socialite\Facades\Socialite;

Route::get('/auth/redirect', function () {
return Socialite::driver('cognito')->redirect();
});

Этот метод redirect() вызывает метод getAuthUrl(), где пользователь перенаправляется на страницу аутентификации поставщика. Итак, нам нужно указать этот URL в этом методе. Мы также извлекаем, как мы получаем базовый URL другим методом, так как мы собираемся использовать его в разных местах:

/**
* @return string
*/

public function getCognitoUrl()
{
return config('services.cognito.base_uri') . '/oauth2';
}

/**
* @param string $state
*
* @return string
*/

protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase($this->getCognitoUrl() . '/authorize', $state);
}

Внутренний метод buildAuthUrlFromBase() создаёт URL аутентификации со всеми необходимыми параметрами.

Как только пользователь проходит аутентификацию у стороннего поставщика, он перенаправляется на callback URL, который мы определяем в нашем приложении. Это зависит от того, что вы хотите сделать с этим методом контроллера, но вы, вероятно вызовите метод Socialite user(), например:

Route::get('/auth/callback', function () {
$user = Socialite::driver('cognito')->user();

// $user->token
});

Когда вы вызываете этот метод, он вызывает метод getTokenUrl() для получения токена доступа с заданным кодом из параметров callback URL. Итак, нам нужно предоставить этот URL:

/**
* @return string
*/

protected function getTokenUrl()
{
return $this->getCognitoUrl() . '/token';
}

Когда у нас есть токен доступа, мы можем получить аутентифицированного пользователя, что мы и сделаем в методе getUserByToken(). В нашем случае нужно сделать POST запрос следующим образом:

/**
* @param string $token
*
* @throws GuzzleException
*
* @return array|mixed
*/

protected function getUserByToken($token)
{
$response = $this->getHttpClient()->post($this->getCognitoUrl() . '/userInfo', [
'headers' => [
'cache-control' => 'no-cache',
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/x-www-form-urlencoded',
],
]);

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

Наконец, мы получаем пользовательский объект из предыдущего метода, и нам нужно отобразить этот объект в новый класс User. В нашем случае мы используем Laravel\Socialite\Two\User и сопоставляем пользователя с помощью mapUserToObject(), таким образом:

/**
* @return User
*/

protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user)->map([
'id' => $user['sub'],
'email' => $user['email'],
'username' => $user['username'],
'email_verified' => $user['email_verified'],
'family_name' => $user['family_name'],
]);
}

Теперь в вашем callback() методе вы должны сделать что-то вроде этого:

Route::get('/auth/callback', function () {
try {
$cognitoUser = Socialite::driver('cognito')->user();
$user = User::query()->whereEmail($cognitoUser->email)->first();

if (!$user) {
return redirect('login');
}

Auth::guard('web')->login($user);

return redirect(route('home'));
} catch (Exception $exception) {
return redirect('login');
}
});

В зависимости от провайдера может потребоваться добавить некоторые области в запрос проверки подлинности. Области — это механизм ограничения доступа пользователя к приложению.

В AWS Cognito есть области зарезервированные системой: openid, email, phone, profile, и aws.cognito.signin.user.admin. Больше об этих областях можно узнать из документации. Вы также можете создавать настраиваемы области в Cognito, более подробная информация о них в документации

В классе SocialiteCognitoProvider вы можете определить настраиваемые области видимости, переопределив внутренние переменные $scopes и $scopeSeparator следующим образом:

class SocialiteCognitoProvider extends AbstractProvider
{
/**
* @var string[]
*/

protected $scopes = [
'openid',
'profile',
'aws.cognito.signin.user.admin',
];

/**
* @var string
*/

protected $scopeSeparator = ' ';

// ...
}

Более подробно об областях AWS Cognito можно узнать из официальной документации

Окончательно класс будет выглядеть так:

// ...

use Laravel\Socialite\Two\User;
use GuzzleHttp\Exception\GuzzleException;
use Laravel\Socialite\Two\AbstractProvider;

class SocialiteCognitoProvider extends AbstractProvider
{
/**
* @var string[]
*/

protected $scopes = [
'openid',
'profile',
'aws.cognito.signin.user.admin',
];

/**
* @var string
*/

protected $scopeSeparator = ' ';

/**
* @return string
*/

public function getCognitoUrl()
{
return config('services.cognito.base_uri') . '/oauth2';
}

/**
* @param string $state
*
* @return string
*/

protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase($this->getCognitoUrl() . '/authorize', $state);
}

/**
* @return string
*/

protected function getTokenUrl()
{
return $this->getCognitoUrl() . '/token';
}

/**
* @param string $token
*
* @throws GuzzleException
*
* @return array|mixed
*/

protected function getUserByToken($token)
{
$response = $this->getHttpClient()->post($this->getCognitoUrl() . '/userInfo', [
'headers' => [
'cache-control' => 'no-cache',
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/x-www-form-urlencoded',
],
]);

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

/**
* @return User
*/

protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user)->map([
'id' => $user['sub'],
'email' => $user['email'],
'username' => $user['username'],
'email_verified' => $user['email_verified'],
'family_name' => $user['family_name'],
]);
}
}

Но как Socialite узнаете о драйвере? Нужно добавить код в AppServiceProvider:

// ...
use Laravel\Socialite\Contracts\Factory;

/**
* @throws BindingResolutionException
*/

public function boot()
{
$socialite = $this->app->make(Factory::class);

$socialite->extend('cognito', function () use ($socialite) {
$config = config('services.cognito');

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

В методе boot() мы регистрируем наш драйвер в диспетчере Socialite, поэтому, когда мы вызываем Socialite::driver('cognito'), он создаёт экземпляр нашего класса SocialiteCognitoProvider.

Вот и всё! Вот так вы реализуете новый пользовательский драйвер для Laravel Socialite. Что бы облегчить вам жизнь, мы создали небольшой пакет для пользовательского драйвера Cognito, который вы можете посмотреть на GitHub.

Дополнительные материалы

Предыдущая Статья

Сравнение Node.js с JavaScript в браузере

Следующая Статья

Как преобразовать Node.js Buffer в String