Чат Telegram
Группа ВКонтакте
Авторизация в Symfony через социальные сети. Авторизация через Github

Авторизация в 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 и отношениями между сущностями.

loader
Логические задачи с собеседований