first commit
This commit is contained in:
0
src/Controller/.gitignore
vendored
Normal file
0
src/Controller/.gitignore
vendored
Normal file
17
src/Controller/MainController.php
Normal file
17
src/Controller/MainController.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?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;
|
||||
|
||||
class MainController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function home(Request $request): Response
|
||||
{
|
||||
return new Response('<h1>Hello world</h1>');
|
||||
}
|
||||
}
|
0
src/Entity/.gitignore
vendored
Normal file
0
src/Entity/.gitignore
vendored
Normal file
152
src/Hydra/Client.php
Normal file
152
src/Hydra/Client.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra;
|
||||
|
||||
use App\Hydra\Exception\InvalidChallengeException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
class Client
|
||||
{
|
||||
private const int MAX_RETRY = 3;
|
||||
private const array SLEEP_TIME = [
|
||||
5,
|
||||
500,
|
||||
5000,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $client,
|
||||
private readonly string $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
|
||||
{
|
||||
$attempt = 0;
|
||||
while ($attempt < self::MAX_RETRY) {
|
||||
$response = $this->client->request(
|
||||
'GET',
|
||||
$this->hydraAdminBaseUrl.'/oauth2/auth/requests/consent',
|
||||
[
|
||||
'query' => [
|
||||
'consent_challenge' => $consentChallenge,
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
if (503 === $status) {
|
||||
++$attempt;
|
||||
usleep(1000 * self::SLEEP_TIME[$attempt] + random_int(1, 5) * 1000);
|
||||
continue;
|
||||
}
|
||||
switch ($status) {
|
||||
case 404:
|
||||
throw new InvalidChallengeException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (self::MAX_RETRY === $attempt) {
|
||||
throw new \Exception(sprintf('Fetch consent a rencontré une erreur %s après %s tentatives', $response->getStatusCode(), self::MAX_RETRY));
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
}
|
80
src/Hydra/HydraService.php
Normal file
80
src/Hydra/HydraService.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Hydra;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Hydra\Exception\InvalidChallengeException;
|
||||
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\RequestStack;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
|
||||
class HydraService extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Client $client,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly TokenStorageInterface $tokenStorage,
|
||||
private readonly string $baseUrl
|
||||
){
|
||||
}
|
||||
|
||||
public function handleLoginRequest(Request $request): RedirectResponse
|
||||
{
|
||||
$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->requestStack->getSession()->clear();
|
||||
throw new BadRequestException();
|
||||
}
|
||||
// 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->requestStack->getSession()->set('challenge', $loginRequestInfo['challenge']);
|
||||
|
||||
return new RedirectResponse($this->baseUrl.'/connect/login-accept');
|
||||
}
|
||||
|
||||
public function handleConsentRequest(Request $request): RedirectResponse
|
||||
{
|
||||
$challenge = $request->query->get('consent_challenge');
|
||||
if (!$challenge) {
|
||||
throw new BadRequestException("Le challenge n'est pas disponible");
|
||||
}
|
||||
|
||||
$consentRequestInfo = $this->client->fetchConsentRequestInfo($challenge)->toArray();
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new AccessDeniedException('Utilisateur non autorisé.');
|
||||
}
|
||||
$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): RedirectResponse
|
||||
{
|
||||
$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->requestStack->getSession()->clear();
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
return new RedirectResponse($logoutAcceptRes['redirect_to']);
|
||||
}
|
||||
}
|
11
src/Kernel.php
Normal file
11
src/Kernel.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
|
||||
class Kernel extends BaseKernel
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
}
|
0
src/Repository/.gitignore
vendored
Normal file
0
src/Repository/.gitignore
vendored
Normal file
Reference in New Issue
Block a user