Авторизация в Symfony через социальные сети. Часть 2: авторизация через Github
В прошлой статье мы реализовали с вами авторизацию через Google. Для этого мы скачали готовый бандл, зарегистрировали наше приложение, получили необходимые данные, после чего могли убедиться в том, что это работает. В этой статье, как и планировалось, мы реализуем авторизацию через Github.
Создание приложения
Чтобы воспользоваться API гитхаба, нам так же, как и с Google, нужно зарегистрировать приложение и получить наш публичный id и секретный ключ, по которым Github будет нас идентифицировать и давать доступ нашему приложению к данным его пользователей. Для этого перейдите по следующей ссылке. В поле Homepage URL напишем адрес нашего локального приложения, то есть http://localhost:8000, а в Authorization callback URL - http://localhost:8000/github/auth.
Конфигурация
Как видите, протокол OAuth работает везде одинаково. После создания приложения вы получите Client ID и Client Secret. Сохраняем их в .env файле под следующими ключевыми словами:
OAUTH_GITHUB_CLIENT_ID=
OAUTH_GITHUB_CLIENT_SECRET=
А в файл config/packages/knpu_aouth2_client.yaml добавляем следующие настройки:
github:
type: github
client_id: '%env(OAUTH_GITHUB_CLIENT_ID)%'
client_secret: '%env(OAUTH_GITHUB_CLIENT_SECRET)%'
redirect_route: github_auth
redirect_params: {}
Теперь наше приложение готово, осталось написать новый Guard, который будет обрабатывать соответствующий роут, куда будут приходить данные о пользователе, и сохранять данные в базу, а также аутентифировать его.
Реализация
По примеру из прошлого урока добавим 2 таких же экшена для Github в наш OAuthController.
/**
* @Route("/connect/github", name="connect_github_start")
*
* @param ClientRegistry $clientRegistry
*
* @return RedirectResponse
*/
public function redirectToGithubConnect(ClientRegistry $clientRegistry)
{
return $clientRegistry
->getClient('github')
->redirect([
'user', 'public_repo'
]);
}
/**
* @Route("/github/auth", name="github_auth")
*
* @return RedirectResponse|Response
*/
public function authenticateGithubUser()
{
if (!$this->getUser()) {
return new Response('User nof found', 404);
}
return $this->redirectToRoute('blog_posts');
}
В параметрах метода redirect указываем в виде массива скоупы, к которым хотим получать доступ при авторизации нашего пользователя, а именно - данные о самом пользователе и данные его публичных репозиториев.
Теперь напишем наш Guard по такому же приему, как и для Google.
<?php
declare(strict_types=1);
namespace App\Infrastructure\Http\Security\Guard\Authenticators;
use App\Domain\User\Event\CreatedUserEvent;
use App\Domain\User\Model\Entity\User;
use App\Domain\User\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use League\OAuth2\Client\Provider\GithubResourceOwner;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class OAuthGithubAuthenticator extends SocialAuthenticator
{
/**
* @var ClientRegistry
*/
private $clientRegistry;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var UserRepository
*/
private $userRepository;
/**
* @var RouterInterface
*/
private $router;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @param ClientRegistry $clientRegistry
* @param EntityManagerInterface $em
* @param UserRepository $userRepository
* @param RouterInterface $router
* @param EventDispatcherInterface $eventDispatcher
*/
public function __construct(
ClientRegistry $clientRegistry,
EntityManagerInterface $em,
UserRepository $userRepository,
RouterInterface $router,
EventDispatcherInterface $eventDispatcher
) {
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->userRepository = $userRepository;
$this->router = $router;
$this->eventDispatcher = $eventDispatcher;
}
/**
* @param Request $request
* @param AuthenticationException|null $authException
*
* @return RedirectResponse|Response
*/
public function start(
Request $request,
AuthenticationException $authException = null
): Response
{
return new RedirectResponse($this->router->generate('login'));
}
/**
* @param Request $request
*
* @return bool
*/
public function supports(Request $request)
{
return $request->attributes->get('_route') === 'github_auth';
}
/**
* @param Request $request
*
* @return AccessToken|mixed
*/
public function getCredentials(Request $request)
{
return $this->fetchAccessToken($this->getGithubClient());
}
/**
* @param mixed $credentials
* @param UserProviderInterface $userProvider
*
* @return User|null|UserInterface
*
* @throws Exception
*/
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
/** @var GithubResourceOwner $githubUser */
$githubUser = $this->getGithubClient()
->fetchUserFromToken($credentials);
$clientId = $githubUser->getId();
/** @var User $existingUser */
$existingUser = $this->userRepository
->findOneBy(['clientId' => $clientId]);
if ($existingUser) {
return $existingUser;
}
$githubUserData = $githubUser->toArray();
$user = User::fromGithubRequest(
(string) $clientId,
$githubUserData['email'] ?? $githubUserData['login'],
$githubUserData['name']
);
$this->em->persist($user);
$this->em->flush();
return $user;
}
/**
* @param Request $request
* @param AuthenticationException $exception
*
* @return null|Response
*/
public function onAuthenticationFailure(
Request $request,
AuthenticationException $exception
): ?Response
{
return new Response('Authentication failed', Response::HTTP_FORBIDDEN);
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $providerKey
*
* @return null|Response
*/
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
$providerKey
): ?Response
{
return new RedirectResponse($this->router->generate('proglib_app'));
}
/**
* @return OAuth2Client
*/
private function getGithubClient(): OAuth2Client
{
return $this->clientRegistry->getClient('github');
}
}
Как вы могли заметить, тут есть небольшая разница с прошлым Guard'ом, а именно - методы onAuthenticationFailure и onAuthenticationSuccess теперь возвращают не null, а конкретный респонс. Для чего это было сделано? Дело в том, что если вам понадобится реализовать функциональность remember_me и хранить данные об авторизации пользователя не только в сессии, но и в куках, вам нужно вернуть конкретный респонс из этих методов, как просит Symfony.
Теперь нам осталось настроить файл config/packages/security.yaml:
security:
providers:
user_provider:
entity:
class: App\Domain\User\Model\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
remember_me:
secret: '%kernel.secret%'
lifetime: 31536000
always_remember_me: true
anonymous: true
guard:
entry_point: App\Security\OAuthGoogleAuthenticator
authenticators:
- App\Security\OAuthGoogleAuthenticator
- App\Security\OAuthGithubAuthenticator
logout:
path: logout
Помимо того, что мы добавили новый Guard в наш список, мы также добавили такую настройку как entry_point. Дело в том, что когда у нас в системе есть несколько разных аутентификаторов, занимающихся авторизацией по апи, через социальные сети или с простой формы, нам нужно определить один из них как главный, прописав его в entry_point. Также мы добавили функциональность remember_me, благодаря которой пользователь будет аутентифицирован целый год.
Теперь у вас можем выскочить ошибка при авторизации через гугл, так как методы onAuthenticationFailure и onAuthenticationSuccess по-прежнему возвращают null. Исправим это:
/**
* @param Request $request
* @param AuthenticationException $exception
*
* @return null|Response
*/
public function onAuthenticationFailure(
Request $request,
AuthenticationException $exception
): ?Response
{
return new Response('Authentication failed', 403);
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $providerKey
*
* @return null|Response
*/
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
$providerKey
): ?Response
{
return new RedirectResponse($this->router->generate('blog_posts'));
}
На этом все. В следующих статьях мы начнем глубже знакомиться с Doctrine и отношениями между сущностями.
Комментарии