Laravel: Создание драйвера для Laravel Socialite
В нашем случае мы хотим использовать 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.