授权后,Discord oAuth2登录

时间:2019-09-01 16:55:31

标签: oauth-2.0 discord symfony4

我有一个symfony4应用程序,并且正在使用knpuniversity / oauth2-client-bundle来根据不和谐的oAuth端点对用户进行身份验证。

用户单击“通过Discord登录”按钮,他会看到discord的授权页面,接受该页面,现在已登录到我的页面上。
到目前为止一切顺利。

这是行不通的:
用户在另一台计算机上,因此我的页面上没有会话。他登录到不和谐的Web客户端。之后,他访问了我的页面,但是他没有登录。 当他再次单击“通过Discord登录”时,他会再次看到授权页面,而不是直接在我的页面上登录。

也许我在这里出错了,但是通常当我在google,facebook或其他任何东西上使用oAuth登录时,就像在stackoverflow上一样,我再也看不到授权页面。我单击“使用XY登录”,并且只要登录到相应的帐户,我也将立即在另一页上登录。

<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
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\UserProviderInterface;

class DiscordAuthenticator extends SocialAuthenticator
{
    /**
     * @var \KnpU\OAuth2ClientBundle\Client\ClientRegistry
     */
    private $clientRegistry;

    /**
     * @var \App\Repository\UserRepository
     */
    private $repository;

    /**
     * @var \Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
     * DiscordAuthenticator constructor.
     *
     * @param \KnpU\OAuth2ClientBundle\Client\ClientRegistry $clientRegistry
     * @param \App\Repository\UserRepository                 $repository
     * @param \Symfony\Component\Routing\RouterInterface     $router
     */
    public function __construct(
        ClientRegistry $clientRegistry,
        UserRepository $repository,
        RouterInterface $router
    ) {
        $this->clientRegistry = $clientRegistry;
        $this->repository = $repository;
        $this->router = $router;
    }

    /**
     * @inheritDoc
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse('/login/', Response::HTTP_TEMPORARY_REDIRECT);
    }

    /**
     * @inheritDoc
     */
    public function supports(Request $request): bool
    {
        return $request->attributes->get('_route') === 'discord_set_token';
    }

    /**
     * @inheritDoc
     */
    public function getCredentials(Request $request)
    {
        return $this->fetchAccessToken($this->getDiscordClient());
    }

    /**
     * @inheritDoc
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        try {
            /** @var \Wohali\OAuth2\Client\Provider\DiscordResourceOwner $discordUser */
            $discordUser = $this->getDiscordClient()
                                ->fetchUserFromToken($credentials);

            $email = $discordUser->getEmail();

            $existingUser = $this->repository->findOneBy(
                [
                    'externalId'       => $discordUser->getId(),
                    'externalIdSource' => 'discord',
                ]
            );

            if ($existingUser) {
                return $existingUser;
            }

            $user = $this->repository->findOneBy(['email' => $email]);

            if (!$user) {
                $user = new User(
                    $discordUser->getId(),
                    'discord',
                    $discordUser->getUsername(),
                    $discordUser->getEmail()
                );
                $user->setExternalId($discordUser->getId());
                $user->setExternalIdSource('discord');
                $user->setToken($credentials->getToken());
                $user->setRefreshToken($credentials->getRefreshToken());
                $user->setTokenExpiresFromTimestamp($credentials->getExpires());
            }

            $this->repository->persist($user);

            return $user;
        } catch (\Throwable $e) {
            throw new AuthenticationException($e->getMessage(), $e->getCode(), $e);
        }
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    /**
     * @inheritDoc
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): Response
    {
        $targetUrl = $this->router->generate('home');

        return new RedirectResponse($targetUrl);
    }

    /**
     * @return \KnpU\OAuth2ClientBundle\Client\OAuth2Client
     */
    private function getDiscordClient(): OAuth2Client
    {
        return $this->clientRegistry
            ->getClient('discord');
    }
}
<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
    /**
     * @var \App\Repository\UserRepository
     */
    private $repository;

    /**
     * UserProvider constructor.
     *
     * @param \App\Repository\UserRepository $repository
     */
    public function __construct(UserRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * @inheritDoc
     */
    public function loadUserByUsername($username): UserInterface
    {
        $user = $this->repository->findOneBy(['email' => $username]);

        if (!$user) {
            throw new UsernameNotFoundException(sprintf('No User with username %s found', $username));
        }

        return $user;
    }

    /**
     * @inheritDoc
     */
    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        // TODO find out how to use the refresh token to get a new one

        return $user;
    }

    /**
     * @inheritDoc
     */
    public function supportsClass($class): bool
    {
        return $class === User::class;
    }
}
security:
    providers:
        user_provider:
            entity:
                class: App:User
                property: username
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true

            logout:
                path: /logout
                target: /login

            guard:
                authenticators:
                    - App\Security\DiscordAuthenticator

    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
<?php

namespace App\Repository;

use App\Entity\User;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManager;

/**
 * Class UserRepository
 */
class UserRepository
{
    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $entityManager;

    /**
     * @var \Doctrine\ORM\EntityRepository
     */
    private $repository;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->repository = $entityManager->getRepository(User::class);
    }

    /**
     * Finds an entity by its primary key / identifier.
     *
     * @param int $id
     *
     * @return \App\Entity\User|null
     */
    public function find(int $id): ?User
    {
        return $this->repository->find($id);
    }

    /**
     * Finds all entities in the repository.
     *
     * @return \App\Entity\User[]
     */
    public function findAll(): iterable
    {
        return $this->repository->findAll();
    }

    /**
     * Finds entities by a set of criteria.
     *
     * @param array      $criteria
     * @param array|null $orderBy
     * @param int|null   $limit
     * @param int|null   $offset
     *
     * @return \App\Entity\User[]
     */
    public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): iterable
    {
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
    }

    /**
     * Finds a single entity by a set of criteria.
     *
     * @param array      $criteria
     * @param array|null $orderBy
     *
     * @return \App\Entity\User|null
     */
    public function findOneBy(array $criteria, array $orderBy = null): ?User
    {
        return $this->repository->findOneBy($criteria, $orderBy);
    }

    /**
     * Counts entities by a set of criteria.
     *
     * @param array $criteria
     *
     * @return int
     */
    public function count(array $criteria): int
    {
        return $this->repository->count($criteria);
    }

    /**
     * Select all elements from a selectable that match the expression and
     * return a new collection containing these elements.
     *
     * @param \Doctrine\Common\Collections\Criteria $criteria
     *
     * @return \App\Entity\User[]
     */
    public function matching(Criteria $criteria): iterable
    {
        return $this->repository->matching($criteria);
    }

    /**
     * @param \App\Entity\User $user
     *
     * @throws \Doctrine\ORM\ORMException
     * @throws \Doctrine\ORM\OptimisticLockException
     */
    public function persist(User $user): void
    {
        $this->entityManager->merge($user);
        $this->entityManager->flush();
    }
}
<?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route(path="discord/")
 */
class DiscordOAuthController extends AbstractController
{
    /**
     * @var \KnpU\OAuth2ClientBundle\Client\ClientRegistry
     */
    private $clientRegistry;

    /**
     * DiscordOAuthController constructor.
     *
     * @param \KnpU\OAuth2ClientBundle\Client\ClientRegistry $clientRegistry
     */
    public function __construct(ClientRegistry $clientRegistry)
    {
        $this->clientRegistry = $clientRegistry;
    }

    /**
     * @Route(name="discord_redirect_authorization", path="redirect_authorization")
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function redirectAuthorizationAction(): Response
    {
        return $this->clientRegistry->getClient('discord')
                                    ->redirect(['identify', 'email', 'guilds']);
    }

    /**
     * @Route(name="discord_set_token", path="set_token")
     *
     * @return void
     */
    public function setTokenAction(): void
    {
        // empty as authenticator will handle the request
    }
}

1 个答案:

答案 0 :(得分:0)

在我从事这项工作的时候,我没有太多时间,只是简要地浏览了文档。

现在我有更多时间,花一些时间阅读Discords oAuth的整个文档。

然后我发现查询参数prompt,将其设置为none时只会请求一次授权,否则将直接重定向到redirect_uri