mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: implement OAuth 2.0 login flow
This commit is contained in:
@@ -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)]);
|
||||
}
|
||||
}
|
||||
87
src/Security/OAuthAuthenticator.php
Normal file
87
src/Security/OAuthAuthenticator.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
58
src/Security/OAuthProvider.php
Normal file
58
src/Security/OAuthProvider.php
Normal 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);
|
||||
}
|
||||
}
|
||||
40
src/Security/OAuthResourceOwner.php
Normal file
40
src/Security/OAuthResourceOwner.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user