feat: implement OAuth 2.0 login flow

This commit is contained in:
Maël Gangloff
2024-07-22 02:17:42 +02:00
parent c48f37696c
commit 9e8523fa53
12 changed files with 850 additions and 2 deletions

View File

@@ -2,9 +2,13 @@
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\User\UserInterface;
class HomeController extends AbstractController
{
@@ -16,4 +20,15 @@ class HomeController extends AbstractController
}
#[Route(path: "/login/oauth", name: "connect_start")]
public function connectAction(ClientRegistry $clientRegistry): Response
{
return $clientRegistry->getClient('oauth')->redirect();
}
#[Route(path: "/login/oauth/token", name: "login_oauth_token")]
public function getToken(UserInterface $user, JWTTokenManagerInterface $JWTManager): JsonResponse
{
return new JsonResponse(['token' => $JWTManager->create($user)]);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
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\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEntrypointInterface
{
public function __construct(
private readonly ClientRegistry $clientRegistry,
private readonly UserRepository $userRepository,
private readonly EntityManagerInterface $em,
private readonly RouterInterface $router
)
{
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request): ?bool
{
return $request->attributes->get('_route') === 'oauth_connect_check';
}
public function authenticate(Request $request): Passport
{
$client = $this->clientRegistry->getClient('oauth');
$accessToken = $this->fetchAccessToken($client);
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
/** @var OAuthResourceOwner $userFromToken */
$userFromToken = $client->fetchUserFromToken($accessToken);
$existingUser = $this->userRepository->findOneBy(['email' => $userFromToken->getEmail()]);
if ($existingUser) return $existingUser;
$user = new User();
$user->setEmail($userFromToken->getEmail());
$this->em->persist($user);
$this->em->flush();
return $user;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): RedirectResponse
{
return new RedirectResponse($this->router->generate('index'));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_UNAUTHORIZED);
}
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
return new RedirectResponse(
'/login/oauth',
Response::HTTP_TEMPORARY_REDIRECT
);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Security;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
class OAuthProvider extends AbstractProvider
{
use BearerAuthorizationTrait;
public function __construct(private readonly array $options = [], array $collaborators = [])
{
parent::__construct($options, $collaborators);
}
public function getBaseAuthorizationUrl(): string
{
return $this->options['baseAuthorizationUrl'];
}
public function getBaseAccessTokenUrl(array $params): string
{
return $this->options['baseAccessTokenUrl'];
}
public function getResourceOwnerDetailsUrl(AccessToken $token): string
{
return $this->options['resourceOwnerDetailsUrl'];
}
protected function getDefaultScopes(): array
{
return explode(',', $this->options['scope']);
}
protected function checkResponse(ResponseInterface $response, $data): void
{
if ($response->getStatusCode() >= 400) {
throw new IdentityProviderException(
$data['error'] ?? 'Unknown error',
$response->getStatusCode(),
$response
);
}
}
protected function createResourceOwner(array $response, AccessToken $token): ResourceOwnerInterface
{
return new OAuthResourceOwner($response);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Security;
namespace App\Security;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use League\OAuth2\Client\Tool\ArrayAccessorTrait;
class OAuthResourceOwner implements ResourceOwnerInterface
{
use ArrayAccessorTrait;
public array $response;
public function __construct(array $response)
{
$this->response = $response;
}
public function getId(): string
{
return $this->response['sub'];
}
public function toArray(): array
{
return $this->response;
}
public function getEmail(): string
{
return $this->response['email'];
}
public function getName(): string
{
return $this->response['name'];
}
}