diff --git a/.idea/Easy_solution.iml b/.idea/Easy_solution.iml index b06ea2d..06ed20b 100644 --- a/.idea/Easy_solution.iml +++ b/.idea/Easy_solution.iml @@ -5,6 +5,9 @@ + + + diff --git a/.idea/php.xml b/.idea/php.xml index b4ef159..aa5ce23 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -161,6 +161,9 @@ + + + diff --git a/.idea/symfony2.xml b/.idea/symfony2.xml index bd98e40..3298060 100644 --- a/.idea/symfony2.xml +++ b/.idea/symfony2.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/composer.json b/composer.json index a7c56a0..e55871b 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-bundle": "^2.14", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", + "firebase/php-jwt": "^6.11", "league/oauth2-server-bundle": "^0.11.0", "nelmio/cors-bundle": "^2.5", "phpdocumentor/reflection-docblock": "^5.6", diff --git a/composer.lock b/composer.lock index a8bcc6b..2105b46 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0146b1350b6329146b85a9251521ffed", + "content-hash": "ec18ea9254732e2793a2fc5e0e6919aa", "packages": [ { "name": "composer/semver", @@ -1438,6 +1438,69 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, { "name": "lcobucci/clock", "version": "3.3.1", @@ -5259,6 +5322,173 @@ ], "time": "2025-04-04T09:50:51+00:00" }, + { + "name": "symfony/mercure", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/mercure.git", + "reference": "304cf84609ef645d63adc65fc6250292909a461b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mercure/zipball/304cf84609ef645d63adc65fc6250292909a461b", + "reference": "304cf84609ef645d63adc65fc6250292909a461b", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/deprecation-contracts": "^2.0|^3.0|^4.0", + "symfony/http-client": "^4.4|^5.0|^6.0|^7.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0|^7.0", + "symfony/polyfill-php80": "^1.22", + "symfony/web-link": "^4.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "lcobucci/jwt": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0|^7.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "symfony/stopwatch": "^4.4|^5.0|^6.0|^7.0", + "twig/twig": "^2.0|^3.0|^4.0" + }, + "suggest": { + "symfony/stopwatch": "Integration with the profiler performances" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/dunglas/mercure", + "name": "dunglas/mercure" + }, + "branch-alias": { + "dev-main": "0.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mercure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mercure Component", + "homepage": "https://symfony.com", + "keywords": [ + "mercure", + "push", + "sse", + "updates" + ], + "support": { + "issues": "https://github.com/symfony/mercure/issues", + "source": "https://github.com/symfony/mercure/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/dunglas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/mercure", + "type": "tidelift" + } + ], + "time": "2024-04-08T12:51:34+00:00" + }, + { + "name": "symfony/mercure-bundle", + "version": "v0.3.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/mercure-bundle.git", + "reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mercure-bundle/zipball/77435d740b228e9f5f3f065b6db564f85f2cdb64", + "reference": "77435d740b228e9f5f3f065b6db564f85f2cdb64", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.4|^4.0|^5.0", + "php": ">=7.1.3", + "symfony/config": "^4.4|^5.0|^6.0|^7.0", + "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0|^7.0", + "symfony/mercure": "^0.6.1", + "symfony/web-link": "^4.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.3.7|^5.0|^6.0|^7.0", + "symfony/stopwatch": "^4.3.7|^5.0|^6.0|^7.0", + "symfony/ux-turbo": "*", + "symfony/var-dumper": "^4.3.7|^5.0|^6.0|^7.0" + }, + "suggest": { + "symfony/messenger": "To use the Messenger integration" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MercureBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MercureBundle", + "homepage": "https://symfony.com", + "keywords": [ + "mercure", + "push", + "sse", + "updates" + ], + "support": { + "issues": "https://github.com/symfony/mercure-bundle/issues", + "source": "https://github.com/symfony/mercure-bundle/tree/v0.3.9" + }, + "funding": [ + { + "url": "https://github.com/dunglas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/mercure-bundle", + "type": "tidelift" + } + ], + "time": "2024-05-31T09:07:18+00:00" + }, { "name": "symfony/messenger", "version": "v7.2.6", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 686a26c..a525496 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -23,6 +23,9 @@ security: stateless: true oauth2: true provider: app_user_provider + oauth_revoke: + pattern: ^/oauth2/revoke_tokens + stateless: true auth_token: pattern: ^/token stateless: true @@ -40,9 +43,9 @@ security: enable_csrf: true default_target_path: app_index use_referer: true - logout: - path: app_logout - target: app_login +# logout: +# path: app_logout +# target: app_login # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -54,7 +57,9 @@ security: # Note: Only the *first* access control that matches will be used access_control: - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/sso_logout, roles: IS_AUTHENTICATED_FULLY } - { path: ^/token, roles: PUBLIC_ACCESS } + - { path: ^/oauth2/revoke_tokens, roles: PUBLIC_ACCESS } - { path: ^/oauth2/token, roles: PUBLIC_ACCESS } - { path: ^/token, roles: PUBLIC_ACCESS } - { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED } diff --git a/src/Command/ListenSubscritptionCommand.php b/src/Command/ListenSubscritptionCommand.php new file mode 100644 index 0000000..4043af4 --- /dev/null +++ b/src/Command/ListenSubscritptionCommand.php @@ -0,0 +1,89 @@ + 'charles', + 'remoteAddr' => 'rawsfsda' + ]; + $key = "!ChangeThisMercureHubJWTSecretKey!"; + $payload = [ + 'mercure' => [ + 'publish' => ['*'], + 'subscribe' => ['*'], + 'payload' => $tab + ], + ]; + + $jwt = JWT::encode($payload, $key, 'HS256'); + try{ + $response = $this->httpClient->request('GET', $_ENV['MERCURE_URL'] . '/subscriptions', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $jwt, + 'Content-Type' => 'application/ld+json', + ] + ]); + $data = json_decode($response->getContent(), true); + $subscriptions = $this->getSubscription($data); + $encodedSubscriptions = json_encode($subscriptions, JSON_PRETTY_PRINT); + $section = $output->section(); + $section2 = $output->section(); + $section->writeln('Active Subscriptions:'); + $section2->writeln($encodedSubscriptions); + return Command::SUCCESS; + }catch (\Throwable $e){ + $output->writeln('Error fetching subscriptions: ' . $e->getMessage() . ''); + return Command::FAILURE; + } + } + + private function getSubscription(array $data): array { + $subscriptions = []; + foreach ($data['subscriptions'] as $sub) { + if ($sub['active']) { + $subscriptions[] = [ + 'topic' => $sub['topic'], + 'user' => $sub['payload']['user'] ?? null, + 'remoteAddr' => $sub['payload']['remoteAddr'] ?? null, + ]; + } + } + return $subscriptions; + } + + private function getUserFromSubscription(array $subscription): ?string { + $parts = parse_url($subscription['topic']); + $userIdentifier = null; + if (isset($parts['query'])) { + parse_str($parts['query'], $query); + if (isset($query['userId'])) { + $userIdentifier = $query['userId']; + } + } + return $userIdentifier; + } + + private function addSubscriptionToDb(array $subscription): void { +// try{ +// +// } + } + +} \ No newline at end of file diff --git a/src/Controller/IndexController.php b/src/Controller/IndexController.php index 7f7f085..927ebc0 100644 --- a/src/Controller/IndexController.php +++ b/src/Controller/IndexController.php @@ -2,15 +2,19 @@ namespace App\Controller; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; final class IndexController extends AbstractController { #[Route('/', name: 'app_index')] - public function index(): Response + public function index(Request $request, LoggerInterface $logger): Response { + $logger->info("SESSION ID: " . $request->getSession()->getId()); return $this->render('index/index.html.twig', [ 'controller_name' => 'IndexController', ]); diff --git a/src/Controller/OAuth2Controller.php b/src/Controller/OAuth2Controller.php index 1bf0971..c614716 100644 --- a/src/Controller/OAuth2Controller.php +++ b/src/Controller/OAuth2Controller.php @@ -2,20 +2,24 @@ namespace App\Controller; -use App\Entity\User; +use App\Service\AccessTokenService; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -use League\Bundle\OAuth2ServerBundle\Security\Authentication\Token\OAuth2Token; -use League\Bundle\OAuth2ServerBundle\Model\AccessToken; -use League\Bundle\OAuth2ServerBundle\Manager\AccessTokenManagerInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; class OAuth2Controller extends AbstractController { + + #[Route('/oauth2/userinfo', name: 'userinfo', methods: ['GET'])] public function userinfo(Request $request): JsonResponse { @@ -53,4 +57,19 @@ class OAuth2Controller extends AbstractController ]; return $this->json($jwks); } + + #[Route(path: '/oauth2/revoke_tokens', name: 'revoke_tokens', methods: ['POST'])] + public function revokeTokens(Security $security, Request $request, AccessTokenService $accessTokenService, LoggerInterface $logger): Response{ + //Check if the user have valid access token + $data = json_decode($request->getContent(), true); + $userIdentifier = $data['user_identifier']; + if (!$userIdentifier) { + return new JsonResponse(["ERROR" => "User identifier is required"], Response::HTTP_BAD_REQUEST); + } + $accessTokenService->revokeTokens($userIdentifier); + $logger->info("Revoke tokens successfully"); + + return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK); + } + } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 1c5130d..8567bae 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -2,7 +2,14 @@ namespace App\Controller; +use App\Service\AccessTokenService; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use SessionHandlerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Bundle\SecurityBundle\Security; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; @@ -35,10 +42,43 @@ class SecurityController extends AbstractController ]); } - #[Route(path: '/logout', name: 'app_logout')] - public function logout(): void + #[Route(path: '/sso_logout', name: 'sso_logout')] + public function ssoLogout(RequestStack $stack, LoggerInterface $logger, AccessTokenService $accessTokenService, Security $security): Response { - throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + + try{ + $logger->info("SESSION ID: " . $stack->getSession()->getId()); +// dd($stack->getSession()); + + if( $stack->getSession()->invalidate()){ +// $accessTokenService->revokeTokens(); + $security->logout(false); + $logger->info("Session invalidated successfully"); + } else { + $logger->info("Session already invalidated or not started"); + } + }catch (\Exception $e){ + $logger->log(LogLevel::ERROR, 'Error invalidating session: ' . $e->getMessage()); + } +// try{ +// $security->logout(false); +// }catch (\Exception $e){ +// $logger->log(LogLevel::ERROR, 'Error during logout: ' . $e->getMessage()); +// } +// $security->logout(false); +// try{ +// $logger->info("Destruction de la session"); +// $sessionHandler->destroy("089gn04mts0iiiskfji8sjqr15"); +// }catch(\Exception $e){ +// // Log the error if needed +// $logger->error("Erreur lors de la destruction de la session". $e->getMessage()); +// } + $logger->info("Logout successfully"); + // Redirect back to the client (or to a “you are logged out” page) +// $redirect = $request->query->get('redirect_uri', '/'); + return $this->redirect('/'); +// return new JsonResponse(["SUCCESS" => "Tokens revoked successfully"], Response::HTTP_OK); + } #[Route(path: '/consent', name: 'app_consent')] @@ -59,4 +99,6 @@ class SecurityController extends AbstractController // For GET requests, just show the consent form return $this->render('security/consent.html.twig'); } -} + + +} \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php index 883795c..42d9b8b 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\UserRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -53,6 +55,17 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(options: ['default' => false])] private ?bool $isDeleted = null; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Subscriptions::class, mappedBy: 'users')] + private Collection $subscriptions; + + public function __construct() + { + $this->subscriptions = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -219,4 +232,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { return (string) $this->getId(); } + + /** + * @return Collection + */ + public function getSubscriptions(): Collection + { + return $this->subscriptions; + } + + public function addSubscription(Subscriptions $subscription): static + { + if (!$this->subscriptions->contains($subscription)) { + $this->subscriptions->add($subscription); + $subscription->setUsers($this); + } + + return $this; + } + + public function removeSubscription(Subscriptions $subscription): static + { + if ($this->subscriptions->removeElement($subscription)) { + // set the owning side to null (unless already changed) + if ($subscription->getUsers() === $this) { + $subscription->setUsers(null); + } + } + + return $this; + } } diff --git a/src/Repository/AccessTokenRepository.php b/src/Repository/AccessTokenRepository.php index f5a9529..6ac6134 100644 --- a/src/Repository/AccessTokenRepository.php +++ b/src/Repository/AccessTokenRepository.php @@ -10,9 +10,9 @@ use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; final class AccessTokenRepository implements AccessTokenRepositoryInterface { - private BaseAccessTokenRepository $baseAccessTokenRepository; + private AccessTokenRepositoryInterface $baseAccessTokenRepository; - public function __construct(BaseAccessTokenRepository $baseAccessTokenRepository) + public function __construct(AccessTokenRepositoryInterface $baseAccessTokenRepository) { $this->baseAccessTokenRepository = $baseAccessTokenRepository; } diff --git a/src/Service/AccessTokenService.php b/src/Service/AccessTokenService.php index dfafaef..f557c32 100644 --- a/src/Service/AccessTokenService.php +++ b/src/Service/AccessTokenService.php @@ -2,14 +2,37 @@ namespace App\Service; -use App\Repository\UsersOrganizationsRepository; +use Doctrine\ORM\EntityManagerInterface; +use League\Bundle\OAuth2ServerBundle\Model\AccessToken; +use Symfony\Component\Security\Core\User\UserInterface; class AccessTokenService { - public function __construct() + private EntityManagerInterface $entityManager; + + public function __construct(EntityManagerInterface $entityManager) { + $this->entityManager = $entityManager; + } + + public function revokeTokens(String $userIdentifier): void { + $accessTokens = $this->entityManager->getRepository(AccessToken::class)->findBy(['userIdentifier' => $userIdentifier, 'revoked' => false]); + foreach($accessTokens as $accessToken) { + $accessToken->revoke(); + $this->entityManager->persist($accessToken); + $this->entityManager->flush(); + } + } + + public function getUserFromToken(string $token) + { + $data = json_decode(base64_decode(strtr($token, '-_', '+/')), true); + if (isset($data['user_identifier'])) { + return $data['user_identifier']; + } + return null; } diff --git a/symfony.lock b/symfony.lock index 5996d0f..0993096 100644 --- a/symfony.lock +++ b/symfony.lock @@ -1,4 +1,13 @@ { + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, "doctrine/doctrine-bundle": { "version": "2.14", "recipe": { @@ -181,6 +190,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/mercure-bundle": { + "version": "0.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.3", + "ref": "528285147494380298f8f991ee8c47abebaf79db" + }, + "files": [ + "config/packages/mercure.yaml" + ] + }, "symfony/messenger": { "version": "7.2", "recipe": { diff --git a/templates/base.html.twig b/templates/base.html.twig index d9ae5f6..02bdcf0 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -16,6 +16,21 @@ {% block javascripts %} {% block importmap %}{{ importmap('app') }}{% endblock %} + {% endblock %} @@ -23,7 +38,7 @@ {{ include('elements/navbar.html.twig')}} {% block body %} - + {% endblock %}
diff --git a/templates/elements/navbar.html.twig b/templates/elements/navbar.html.twig index 0ffaf77..cf7bd22 100644 --- a/templates/elements/navbar.html.twig +++ b/templates/elements/navbar.html.twig @@ -72,10 +72,10 @@ #}
- - {{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }} - Deconnexion - +{# #} +{# {{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }} #} +{# Deconnexion#} +{# #} diff --git a/templates/index/index.html.twig b/templates/index/index.html.twig index 05f5194..54c44d4 100644 --- a/templates/index/index.html.twig +++ b/templates/index/index.html.twig @@ -4,9 +4,9 @@ {% block body %} - {% if app.user %} -
- You are logged in as {{ app.user.userIdentifier }}, Logout -
- {% endif %} +{# {% if app.user %}#} +{#
#} +{# You are logged in as {{ app.user.userIdentifier }}, Logout#} +{#
#} +{# {% endif %}#} {% endblock %}