environnement complet autonome, révision complete de la méthode, ajout de configuration
This commit is contained in:
@ -3,171 +3,55 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Form\UserType;
|
||||
use App\Services\PdoServices;
|
||||
use App\Hydra\Client;
|
||||
use App\Hydra\HydraService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class MainController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var Session
|
||||
*/
|
||||
private $session;
|
||||
public HydraService $hydra;
|
||||
public Client $client;
|
||||
public SessionInterface $session;
|
||||
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
|
||||
/**
|
||||
* @var HttpClientInterface
|
||||
*/
|
||||
public $client;
|
||||
private $pdoServices;
|
||||
|
||||
public function __construct(PdoServices $pdoServices, HttpClientInterface $client, SessionInterface $session, UrlGeneratorInterface $router)
|
||||
public function __construct(SessionInterface $session, HydraService $hydra, Client $client)
|
||||
{
|
||||
$this->pdoServices = $pdoServices;
|
||||
$this->session = $session;
|
||||
$this->client = $client;
|
||||
$this->router = $router;
|
||||
$this->hydra = $hydra;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/oauth/login", name="app_login")
|
||||
* @Route("/", name="app_home")
|
||||
*/
|
||||
public function loginOidc(Request $request)
|
||||
public function home(Request $request)
|
||||
{
|
||||
$challenge = $request->query->get('login_challenge');
|
||||
// S'il n'y a pas de challenge, on déclenche une bad request
|
||||
if (!$challenge) {
|
||||
throw new BadRequestException('pas de challenge');
|
||||
}
|
||||
// On vérifie que la requête d'identification provient bien de hydra
|
||||
$response = $this->client->request('GET', $this->getParameter('url_login_challenge').$challenge, [
|
||||
'headers' => [
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
]);
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
$this->session->clear();
|
||||
throw new BadRequestException('pa de code 200');
|
||||
}
|
||||
// si le challenge est validé par hydra, on le stocke en session pour l'utiliser par la suite et on redirige vers une route interne protégée qui va déclencher l'identification FranceConnect
|
||||
$this->session->set('challenge', $challenge);
|
||||
|
||||
return $this->redirectToRoute('oauth_login');
|
||||
return $this->hydra->handleLoginRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/oauth/connect", name="oauth_login")
|
||||
* @Route("/connect/login-accept", name="app_login_accept")
|
||||
*/
|
||||
public function oauth(Request $request)
|
||||
public function loginAccept(Request $request)
|
||||
{
|
||||
if ($request->headers->get('referer') !== $this->router->generate('oauth_login', [], 0) && !in_array($request->headers->get('referer'), $this->getParameter('urlIssuer'))) {
|
||||
throw new BadRequestException('Vous devez passer par le issuer pour vous connecter');
|
||||
}
|
||||
/** @var User */
|
||||
$user = $this->getUser();
|
||||
$loginAcceptRes = $this->client->acceptLoginRequest($this->session->get('challenge'), [
|
||||
'subject' => $user->getLogin(),
|
||||
'remember' => true,
|
||||
])->toArray();
|
||||
|
||||
$user = new User();
|
||||
$loginForm = $this->createForm(UserType::class, $user);
|
||||
$loginForm->handleRequest($request);
|
||||
if ($loginForm->isSubmitted() && $loginForm->isValid()) {
|
||||
$email = $loginForm->get('email')->getData();
|
||||
try {
|
||||
// requête préparée
|
||||
$datas = $this->pdoServices->fetchDatas($email);
|
||||
|
||||
if (!$datas) {
|
||||
// Si le hash du password n'est pas trouvé, c'est que l'email n'existe pas, on retourne la page de login avec une erreur
|
||||
return $this->render('login.html.twig', [
|
||||
'form' => $loginForm->createView(),
|
||||
'error_mail' => 'mail non trouvé',
|
||||
]);
|
||||
}
|
||||
$hashPassword = $datas[$this->getParameter('passwordColumnName')];
|
||||
$password = $loginForm->get('password')->getData();
|
||||
|
||||
if ($this->pdoServices->verifyPassword($password, $hashPassword)) {
|
||||
// On défait la mot de passe qui ne servira plus
|
||||
unset($datas[$this->getParameter('passwordColumnName')]);
|
||||
$this->session->set('datas', $datas);
|
||||
$response = $this->client->request('PUT', $this->getParameter('url_login_challenge_accept').$this->session->get('challenge'), [
|
||||
'json' => [
|
||||
'subject' => $email,
|
||||
'acr' => 'string',
|
||||
],
|
||||
]);
|
||||
// On initie l'acceptation du login challenge émis par hydra et on récupère l'url de redirection
|
||||
$redirect_to = $response->toArray()['redirect_to'];
|
||||
|
||||
return $this->redirect($redirect_to, 301);
|
||||
} else {
|
||||
return $this->render('login.html.twig', [
|
||||
'form' => $loginForm->createView(),
|
||||
'error_password' => 'Le mot de passe est incorrect',
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
dd($e);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('login.html.twig', [
|
||||
'form' => $loginForm->createView(),
|
||||
]);
|
||||
return new RedirectResponse($loginAcceptRes['redirect_to']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/oauth/consent", name="consent")
|
||||
* @Route("/connect/consent", name="app_consent")
|
||||
*/
|
||||
public function consent(Request $request)
|
||||
{
|
||||
$challenge = $request->query->get('consent_challenge');
|
||||
if (!$challenge) {
|
||||
throw new BadRequestException("Le challenge n'est pas disponible");
|
||||
}
|
||||
|
||||
// Vérification du consent_challenge avec hydra
|
||||
$response = $this->client->request('GET', $this->getParameter('url_consent_challenge').$challenge, [
|
||||
'headers' => [
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
]);
|
||||
if (200 !== $response->getStatusCode()) {
|
||||
$this->session->clear();
|
||||
throw new BadRequestException("Le challenge n'est pas authorisé");
|
||||
}
|
||||
$response = $this->client->request('PUT', $this->getParameter('url_consent_challenge_accept').$challenge, [
|
||||
'headers' => [
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
'json' => [
|
||||
'grant_scope' => ['openid', 'offline_access'],
|
||||
'session' => [
|
||||
'id_token' => [
|
||||
'user' => $this->session->get('datas'),
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$redirect_to = $response->toArray()['redirect_to'];
|
||||
|
||||
return $this->redirect($redirect_to, 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/oauth/logout", name="app_logout")
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
$this->session->clear();
|
||||
|
||||
return $this->redirect($this->getParameter('urlLogoutSuccess'));
|
||||
return $this->hydra->handleConsentRequest($request);
|
||||
}
|
||||
}
|
||||
|
34
src/Controller/SecurityController.php
Normal file
34
src/Controller/SecurityController.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/login", name="app_login")
|
||||
*/
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
if ($error) {
|
||||
}
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/logout", name="app_logout")
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
}
|
||||
}
|
25
src/DependencyInjection/PdoConfiguration.php
Normal file
25
src/DependencyInjection/PdoConfiguration.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Pdo\PdoRequest;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
class PdoConfiguration implements ConfigurationInterface
|
||||
{
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder('pdo');
|
||||
$treeBuilder->getRootNode()->children()
|
||||
->scalarNode(PdoRequest::COLUMN_LOGIN_NAME)->isRequired()->cannotBeEmpty()->end()
|
||||
->scalarNode(PdoRequest::COLUMN_PASSWORD_NAME)->isRequired()->cannotBeEmpty()->end()
|
||||
->scalarNode(PdoRequest::TABLE_NAME)->isRequired()->cannotBeEmpty()->end()
|
||||
->arrayNode(PdoRequest::DATA_TO_FETCH)
|
||||
->scalarPrototype()->end()
|
||||
->end()
|
||||
->end();
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
}
|
27
src/DependencyInjection/PdoExtension.php
Normal file
27
src/DependencyInjection/PdoExtension.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Pdo\PdoRequest;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
|
||||
class PdoExtension extends Extension implements CompilerPassInterface
|
||||
{
|
||||
/** @var array */
|
||||
protected $pdoConfig;
|
||||
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
$configuration = new PdoConfiguration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
$this->pdoConfig = $config;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$definition = $container->getDefinition(PdoRequest::class);
|
||||
$definition->replaceArgument('$config', $this->pdoConfig);
|
||||
}
|
||||
}
|
@ -2,25 +2,27 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Validator as AcmeAssert;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User
|
||||
class User implements UserInterface
|
||||
{
|
||||
private string $email;
|
||||
|
||||
/** @var array */
|
||||
protected $attributes;
|
||||
private string $login;
|
||||
private string $password;
|
||||
private string $rememberMe;
|
||||
private bool $rememberMe;
|
||||
|
||||
public function getEmail(): ?string
|
||||
public function __construct($login, $password, $attributes, $rememberMe = false)
|
||||
{
|
||||
return $this->email;
|
||||
$this->password = $password;
|
||||
$this->login = $login;
|
||||
$this->attributes = $attributes;
|
||||
$this->rememberMe = $rememberMe;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
public function getLogin(): ?string
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
return $this->login;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
@ -28,22 +30,37 @@ class User
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): self
|
||||
public function getAttributes(): array
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
public function getRememberMe(): string
|
||||
public function getRememberMe(): bool
|
||||
{
|
||||
return $this->rememberMe;
|
||||
}
|
||||
|
||||
public function setRememberMe(bool $rememberMe): self
|
||||
public function getRoles(): array
|
||||
{
|
||||
$this->rememberMe = $rememberMe;
|
||||
|
||||
return $this;
|
||||
return ['ROLE_USER'];
|
||||
}
|
||||
}
|
||||
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function eraseCredentials()
|
||||
{
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->login;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->login;
|
||||
}
|
||||
}
|
||||
|
36
src/EventListener/LogoutSubscriber.php
Normal file
36
src/EventListener/LogoutSubscriber.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Hydra\HydraService;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
class LogoutSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UrlGeneratorInterface $urlGenerator,
|
||||
private HydraService $hydra
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [LogoutEvent::class => 'onLogout'];
|
||||
}
|
||||
|
||||
public function onLogout(LogoutEvent $event): void
|
||||
{
|
||||
// get the security token of the session that is about to be logged out
|
||||
// get the current request
|
||||
$request = $event->getRequest();
|
||||
|
||||
// get the current response, if it is already set by another listener
|
||||
$response = $event->getResponse();
|
||||
|
||||
// configure a custom logout response to the homepage
|
||||
$response = $this->hydra->handleLogoutRequest($request);
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Validator\ExistingEmail;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
|
||||
class UserType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('email', EmailType::class, [
|
||||
"required"=>true
|
||||
])
|
||||
->add("password", PasswordType::class, [
|
||||
"attr" => ["class" => "password-field"],
|
||||
"required" => true,
|
||||
"label"=>"Mot de passe"
|
||||
])
|
||||
->add('rememberMe', CheckboxType::class, [
|
||||
"required"=> false,
|
||||
"label"=> "Se souvenir de moi"
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
"data_class" => User::class,
|
||||
]);
|
||||
}
|
||||
}
|
139
src/Hydra/Client.php
Normal file
139
src/Hydra/Client.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra;
|
||||
|
||||
use App\Hydra\Exception\InvalidChallengeException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class Client
|
||||
{
|
||||
protected $client;
|
||||
|
||||
protected $hydraAdminBaseUrl;
|
||||
|
||||
public function __construct(HttpClientInterface $client, string $hydraAdminBaseUrl)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->hydraAdminBaseUrl = $hydraAdminBaseUrl;
|
||||
}
|
||||
|
||||
public function fetchLoginRequestInfo(string $loginChallenge): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/login',
|
||||
[
|
||||
'query' => [
|
||||
'login_challenge' => $loginChallenge,
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
switch ($response->getStatusCode()) {
|
||||
case 404:
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function fetchLogoutRequestInfo(string $logoutChallenge): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/logout',
|
||||
[
|
||||
'query' => [
|
||||
'logout_challenge' => $logoutChallenge,
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
switch ($response->getStatusCode()) {
|
||||
case 404:
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function fetchConsentRequestInfo(string $consentChallenge): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/consent',
|
||||
[
|
||||
'query' => [
|
||||
'consent_challenge' => $consentChallenge,
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
switch ($response->getStatusCode()) {
|
||||
case 404:
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function acceptLoginRequest(string $loginChallenge, array $payload): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'PUT',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/login/accept',
|
||||
[
|
||||
'query' => [
|
||||
'login_challenge' => $loginChallenge,
|
||||
],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
'body' => json_encode($payload),
|
||||
]
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function acceptConsentRequest(string $consentChallenge, array $payload): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'PUT',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/consent/accept',
|
||||
[
|
||||
'query' => [
|
||||
'consent_challenge' => $consentChallenge,
|
||||
],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
'body' => json_encode($payload),
|
||||
]
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function acceptLogoutRequest(string $logoutChallenge): ResponseInterface
|
||||
{
|
||||
$response = $this->client->request(
|
||||
'PUT',
|
||||
$this->hydraAdminBaseUrl . '/oauth2/auth/requests/logout/accept',
|
||||
[
|
||||
'query' => [
|
||||
'logout_challenge' => $logoutChallenge,
|
||||
],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
9
src/Hydra/Exception/InvalidChallengeException.php
Normal file
9
src/Hydra/Exception/InvalidChallengeException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra\Exception;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
|
||||
class InvalidChallengeException extends BadRequestException
|
||||
{
|
||||
}
|
9
src/Hydra/Exception/InvalidIssuerException.php
Normal file
9
src/Hydra/Exception/InvalidIssuerException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra\Exception;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
|
||||
class InvalidIssuerException extends BadRequestException
|
||||
{
|
||||
}
|
85
src/Hydra/HydraService.php
Normal file
85
src/Hydra/HydraService.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra;
|
||||
|
||||
use App\Hydra\Exception\InvalidChallengeException;
|
||||
use App\Services\PdoService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class HydraService extends AbstractController
|
||||
{
|
||||
public SessionInterface $session;
|
||||
public UrlGeneratorInterface $router;
|
||||
public Client $client;
|
||||
public PdoService $pdoServices;
|
||||
public TokenStorageInterface $tokenStorage;
|
||||
|
||||
public function __construct(PdoService $pdoServices, Client $client, SessionInterface $session, UrlGeneratorInterface $router, TokenStorageInterface $tokenStorage)
|
||||
{
|
||||
$this->pdoServices = $pdoServices;
|
||||
$this->session = $session;
|
||||
$this->client = $client;
|
||||
$this->router = $router;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
}
|
||||
|
||||
public function handleLoginRequest(Request $request)
|
||||
{
|
||||
$challenge = $request->query->get('login_challenge');
|
||||
// S'il n'y a pas de challenge, on déclenche une bad request
|
||||
if (empty($challenge)) {
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
// Fetch Hydra login request info
|
||||
$res = $this->client->fetchLoginRequestInfo($challenge);
|
||||
$loginRequestInfo = $res->toArray();
|
||||
if (200 !== $res->getStatusCode()) {
|
||||
$this->session->clear();
|
||||
throw new BadRequestException('pas de code 200');
|
||||
}
|
||||
// si le challenge est validé par hydra, on le stocke en session pour l'utiliser par la suite et on redirige vers une route interne protégée qui va déclencher l'identification FranceConnect
|
||||
$this->session->set('challenge', $loginRequestInfo['challenge']);
|
||||
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
|
||||
public function handleConsentRequest(Request $request)
|
||||
{
|
||||
$challenge = $request->query->get('consent_challenge');
|
||||
if (!$challenge) {
|
||||
throw new BadRequestException("Le challenge n'est pas disponible");
|
||||
}
|
||||
|
||||
$consentRequestInfo = $this->client->fetchConsentRequestInfo($challenge)->toArray();
|
||||
/** @var User */
|
||||
$user = $this->getUser();
|
||||
$consentAcceptResponse = $this->client->acceptConsentRequest($consentRequestInfo['challenge'], [
|
||||
'grant_scope' => $consentRequestInfo['requested_scope'],
|
||||
'session' => [
|
||||
'id_token' => $user->getAttributes(),
|
||||
],
|
||||
])->toArray();
|
||||
|
||||
return new RedirectResponse($consentAcceptResponse['redirect_to']);
|
||||
}
|
||||
|
||||
public function handleLogoutRequest(Request $request)
|
||||
{
|
||||
$logoutChallenge = $request->get('logout_challenge');
|
||||
if (empty($logoutChallenge)) {
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
$logoutRequestInfo = $this->client->fetchLogoutRequestInfo($logoutChallenge)->toArray();
|
||||
$logoutAcceptRes = $this->client->acceptLogoutRequest($logoutRequestInfo['challenge'])->toArray();
|
||||
$this->session->clear();
|
||||
$this->tokenStorage->setToken();
|
||||
|
||||
return new RedirectResponse($logoutAcceptRes['redirect_to']);
|
||||
}
|
||||
}
|
@ -2,10 +2,31 @@
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\DependencyInjection\PdoExtension;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
use MicroKernelTrait {
|
||||
registerContainerConfiguration as microKernelConfigureContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function registerContainerConfiguration(LoaderInterface $loader)
|
||||
{
|
||||
$this->microKernelConfigureContainer($loader);
|
||||
|
||||
$loader->load(function (ContainerBuilder $container) use ($loader) {
|
||||
$envLanguage = \getenv('APP_LOCALES');
|
||||
$container->registerExtension(new PdoExtension());
|
||||
$loader->load($this->getConfigDir().'/pdo_configuration/*.{yml,yaml}', 'glob');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
7
src/Pdo/Exception/InvalidPasswordException.php
Normal file
7
src/Pdo/Exception/InvalidPasswordException.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Pdo\Exception;
|
||||
|
||||
class InvalidPasswordException
|
||||
{
|
||||
}
|
@ -9,32 +9,35 @@ class PdoConnect extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @var Singleton
|
||||
* @access private
|
||||
* @static
|
||||
*/
|
||||
private static $_instance = null;
|
||||
|
||||
/**
|
||||
* Constructeur de la classe
|
||||
*
|
||||
* @param void
|
||||
* @return void
|
||||
*/
|
||||
private function __construct() {
|
||||
* Constructeur de la classe
|
||||
*
|
||||
* @param void
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Méthode qui crée l'unique instance de la classe
|
||||
* si elle n'existe pas encore puis la retourne.
|
||||
*
|
||||
* @param void
|
||||
*
|
||||
* @return PdoConnect
|
||||
*/
|
||||
public static function getInstance() {
|
||||
|
||||
if(is_null(self::$_instance)) {
|
||||
self::$_instance = new PdoConnect();
|
||||
public static function getInstance()
|
||||
{
|
||||
if (is_null(self::$_instance)) {
|
||||
self::$_instance = new PdoConnect();
|
||||
}
|
||||
|
||||
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
@ -42,4 +45,4 @@ class PdoConnect extends AbstractController
|
||||
{
|
||||
return new PDO($urlDatabase, $dbUser, $dbPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/Pdo/PdoRequest.php
Normal file
66
src/Pdo/PdoRequest.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Pdo;
|
||||
|
||||
class PdoRequest
|
||||
{
|
||||
public const DATA_TO_FETCH = 'data_to_fetch';
|
||||
public const COLUMN_LOGIN_NAME = 'column_login_name';
|
||||
public const COLUMN_PASSWORD_NAME = 'column_password_name';
|
||||
public const TABLE_NAME = 'table_name';
|
||||
|
||||
protected array $config;
|
||||
protected string $dsn;
|
||||
protected string $user;
|
||||
protected string $password;
|
||||
|
||||
public function __construct(array $config = [], string $dsn, string $user, string $password)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->dsn = $dsn;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getDatabaseDsn()
|
||||
{
|
||||
return $this->dsn;
|
||||
}
|
||||
|
||||
public function getDbUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getDbPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getNameLogin()
|
||||
{
|
||||
return $this->config[self::COLUMN_LOGIN_NAME];
|
||||
}
|
||||
|
||||
public function getNamePassword()
|
||||
{
|
||||
return $this->config[self::COLUMN_PASSWORD_NAME];
|
||||
}
|
||||
|
||||
public function getRequestScope()
|
||||
{
|
||||
$scope = '';
|
||||
foreach ($this->config[self::DATA_TO_FETCH] as $data) {
|
||||
$scope .= $data.',';
|
||||
}
|
||||
$scope = substr($scope, 0, -1);
|
||||
$request = 'SELECT '.$scope.' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::COLUMN_LOGIN_NAME].' = :'.$this->config[self::COLUMN_LOGIN_NAME].';';
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function getRequestLogin()
|
||||
{
|
||||
return 'SELECT '.$this->config[self::COLUMN_PASSWORD_NAME].' FROM '.$this->config[self::TABLE_NAME].' WHERE '.$this->config[self::COLUMN_LOGIN_NAME].' = :'.$this->config[self::COLUMN_LOGIN_NAME].';';
|
||||
}
|
||||
}
|
90
src/Security/PdoUserAuthenticator.php
Normal file
90
src/Security/PdoUserAuthenticator.php
Normal file
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Hydra\Client;
|
||||
use App\Pdo\Exception\InvalidPasswordException;
|
||||
use App\Services\PdoService;
|
||||
use PDOException;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
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;
|
||||
|
||||
class PdoUserAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public const LOGIN_ROUTE = 'app_login';
|
||||
|
||||
private PdoService $pdoService;
|
||||
protected string $baseUrl;
|
||||
private Client $client;
|
||||
private SessionInterface $session;
|
||||
|
||||
public function __construct(string $baseUrl, PdoService $pdoService, Client $client, SessionInterface $session)
|
||||
{
|
||||
$this->baseUrl = $baseUrl;
|
||||
$this->pdoService = $pdoService;
|
||||
$this->client = $client;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
|
||||
{
|
||||
return new RedirectResponse($this->baseUrl.'/connect/login-accept');
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
|
||||
|
||||
return new Response($message, Response::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$login = $request->request->get('login');
|
||||
$password = $request->request->get('password');
|
||||
$rememberMe = $request->request->get('_remember_me') ? true : false;
|
||||
try {
|
||||
// requête préparée
|
||||
$remoteHashedPassword = $this->pdoService->fetchPassword($login);
|
||||
} catch (PDOException $e) {
|
||||
dd($e);
|
||||
}
|
||||
try {
|
||||
$this->pdoService->verifyPassword($password, $remoteHashedPassword);
|
||||
$attributes = $this->pdoService->fetchDatas($login);
|
||||
$user = new User($login, $password, $attributes, $rememberMe);
|
||||
$loader = function (string $userIdentifier) use ($user) {
|
||||
return $user->getLogin() == $userIdentifier ? $user : null;
|
||||
};
|
||||
$passport = new SelfValidatingPassport(new UserBadge($login, $loader));
|
||||
// if ($rememberMe) {
|
||||
// $passport->addBadge(new RememberMeBadge());
|
||||
// }
|
||||
$passport->setAttribute('attributes', $user->getAttributes());
|
||||
|
||||
return $passport;
|
||||
} catch (InvalidPasswordException $e) {
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
}
|
||||
}
|
49
src/Security/PdoUserProvider.php
Normal file
49
src/Security/PdoUserProvider.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class PdoUserProvider implements UserProviderInterface
|
||||
{
|
||||
protected RequestStack $requestStack;
|
||||
|
||||
public function __construct(RequestStack $requestStack, SessionInterface $session)
|
||||
{
|
||||
$this->requestStack = $requestStack;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier, ?User $user): ?UserInterface
|
||||
{
|
||||
if ($user->getUserIdentifier() === $identifier) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function loadUserByUsername(string $username): ?UserInterface
|
||||
{
|
||||
return $this->loadUserByIdentifier($username, null);
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
|
||||
}
|
||||
|
||||
return $this->loadUserByIdentifier($user->getUserIdentifier(), $user);
|
||||
}
|
||||
|
||||
public function supportsClass(string $class)
|
||||
{
|
||||
return User::class === $class || is_subclass_of($class, User::class);
|
||||
}
|
||||
}
|
83
src/Services/PdoService.php
Normal file
83
src/Services/PdoService.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Pdo\Exception\InvalidPasswordException;
|
||||
use App\Pdo\PdoConnect;
|
||||
use App\Pdo\PdoRequest;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
class PdoService extends AbstractController
|
||||
{
|
||||
private $params;
|
||||
public PdoRequest $pdoRequest;
|
||||
|
||||
public function __construct(ParameterBagInterface $params, PdoRequest $pdoRequest)
|
||||
{
|
||||
$this->params = $params;
|
||||
$this->pdoRequest = $pdoRequest;
|
||||
}
|
||||
|
||||
public function fetchDatas($login)
|
||||
{
|
||||
try {
|
||||
$dbh = $this->getConnection();
|
||||
|
||||
// forge de la requête
|
||||
$request = $this->pdoRequest->getRequestScope();
|
||||
|
||||
// Préparation de la requête
|
||||
$query = $dbh->prepare($request);
|
||||
$query->execute([$this->pdoRequest->getNameLogin() => $login]);
|
||||
$datas = $query->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
echo 'Erreur fetch Datas !: '.$e->getMessage().'<br/>';
|
||||
exit();
|
||||
}
|
||||
|
||||
return $datas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $login
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function fetchPassword($login)
|
||||
{
|
||||
try {
|
||||
$dbh = $this->getConnection();
|
||||
$request = $this->pdoRequest->getRequestLogin();
|
||||
$query = $dbh->prepare($request);
|
||||
$query->execute([$this->pdoRequest->getNameLogin() => $login]);
|
||||
$password = $query->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
echo 'Erreur fetch Password!: '.$e->getMessage().'<br/>';
|
||||
exit();
|
||||
}
|
||||
|
||||
return $password[$this->pdoRequest->getNamePassword()];
|
||||
}
|
||||
|
||||
public function getConnection()
|
||||
{
|
||||
// Appel du singleton
|
||||
$pdo = PdoConnect::getInstance();
|
||||
// Connection bdd
|
||||
return $pdo->connect($this->pdoRequest->getDatabaseDsn(), $this->pdoRequest->getDbUser(), $this->pdoRequest->getDbPassword());
|
||||
}
|
||||
|
||||
public function verifyPassword($password, $hashedPassword)
|
||||
{
|
||||
$hashAlgo = $this->params->get('hashAlgo') ?? 'sha256';
|
||||
|
||||
if ($hashedPassword === hash($hashAlgo, $password)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new InvalidPasswordException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Pdo\PdoConnect;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
|
||||
class PdoServices extends AbstractController
|
||||
{
|
||||
private $params;
|
||||
|
||||
public function __construct(ParameterBagInterface $params)
|
||||
{
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public function fetchDatas($email)
|
||||
{
|
||||
try {
|
||||
$pdo = PdoConnect::getInstance();
|
||||
$dbh = $pdo->connect($this->params->get('urlDatabase'), $this->params->get('dbUser'), $this->params->get('dbPassword'));
|
||||
$query = $dbh->prepare($this->getParameter('queryFetchDatas'));
|
||||
$query->execute(['email'=> $email]);
|
||||
$datas = $query->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
print "Erreur !: " . $e->getMessage() . "<br/>";
|
||||
die();
|
||||
}
|
||||
return $datas;
|
||||
}
|
||||
|
||||
public function verifyPassword($password, $hashedPassword)
|
||||
{
|
||||
$hashMethod = $this->params->get('hashMethod');
|
||||
switch ($hashMethod){
|
||||
case "sha1":
|
||||
return $hashedPassword === sha1($password, false);
|
||||
break;
|
||||
case "md5":
|
||||
return $hashedPassword === md5($password);
|
||||
break;
|
||||
case "BCRYPT":
|
||||
default:
|
||||
return password_verify($password, $hashedPassword);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user