set up fonctionnal OAuth2 worklow with consent

This commit is contained in:
Charles 2025-04-23 10:19:06 +02:00
parent fb87a53593
commit 1a30b94863
20 changed files with 310 additions and 56 deletions

4
.env
View File

@ -48,3 +48,7 @@ OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4 OAUTH_PASSPHRASE=8170ea18d2e3e05b5c7ae0672a754bf4
OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066 OAUTH_ENCRYPTION_KEY=f1b7c279f7992205a0df45e295d07066
###< league/oauth2-server-bundle ### ###< league/oauth2-server-bundle ###
###> nelmio/cors-bundle ###
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
###< nelmio/cors-bundle ###

View File

@ -11,7 +11,7 @@
}, },
"@symfony/ux-turbo": { "@symfony/ux-turbo": {
"turbo-core": { "turbo-core": {
"enabled": true, "enabled": false,
"fetch": "eager" "fetch": "eager"
}, },
"mercure-turbo-stream": { "mercure-turbo-stream": {

View File

@ -12,6 +12,7 @@
"doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"league/oauth2-server-bundle": "^0.11.0", "league/oauth2-server-bundle": "^0.11.0",
"nelmio/cors-bundle": "^2.5",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.1",
"symfony/asset": "7.2.*", "symfony/asset": "7.2.*",

64
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2848578c0a7baf9e3d125fcc61becab2", "content-hash": "685b46acb679219276b83724c15afb9d",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@ -2084,6 +2084,68 @@
], ],
"time": "2025-03-24T10:02:05+00:00" "time": "2025-03-24T10:02:05+00:00"
}, },
{
"name": "nelmio/cors-bundle",
"version": "2.5.0",
"source": {
"type": "git",
"url": "https://github.com/nelmio/NelmioCorsBundle.git",
"reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544",
"reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544",
"shasum": ""
},
"require": {
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"mockery/mockery": "^1.3.6",
"symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Nelmio\\CorsBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nelmio",
"homepage": "http://nelm.io"
},
{
"name": "Symfony Community",
"homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors"
}
],
"description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application",
"keywords": [
"api",
"cors",
"crossdomain"
],
"support": {
"issues": "https://github.com/nelmio/NelmioCorsBundle/issues",
"source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0"
},
"time": "2024-06-24T21:25:28+00:00"
},
{ {
"name": "nyholm/psr7", "name": "nyholm/psr7",
"version": "1.8.2", "version": "1.8.2",

View File

@ -16,4 +16,5 @@ return [
Symfony\UX\TogglePassword\TogglePasswordBundle::class => ['all' => true], Symfony\UX\TogglePassword\TogglePasswordBundle::class => ['all' => true],
Symfony\UX\Icons\UXIconsBundle::class => ['all' => true], Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
]; ];

View File

@ -1,30 +1,30 @@
-----BEGIN ENCRYPTED PRIVATE KEY----- -----BEGIN ENCRYPTED PRIVATE KEY-----
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
bqPMXh92TaQXrWxDgzO9Ag== SzDJqo7z76xQc3dcBX8MNQ==
-----END ENCRYPTED PRIVATE KEY----- -----END ENCRYPTED PRIVATE KEY-----

View File

@ -1,9 +1,9 @@
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwQueIPrQEJyma0oiV2wG MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZehUUiAV9vMER+tCpKB
9gl4mpjZQx0QDj/HXyi2hqTjd6z9cfcONmlggD7xoLuiNNmTuVNezHMMC4VNq8/G lLr53bCV2z34r7Qg4gojciR1n7J6esptuLo/JMIYsU1EFmH8dIJ9guc/IufW7hDj
zNQu7Gp18K0uw0WXWWpHtslE3yz9c30FPB4whpz+NMlSXiQEaA2xJxIPxgaMrCG2 iwB2l75brVJ+EG8RRN2R4/IlUXz01a56W+TgLGyVM9iNegfwrNQjEEr3WNnz878h
vc8hMPqiN5pid9ErdkGJLaZd9Q/HqIvVPmw9pVK6HTogfHu61hiaHtA5wDxetFH2 0KQDsG3+kndh60pIuIiiEqG/yoO9C4enyL5XEDcsyKebb5PeCSTewgGm+DY7vj1F
l7V0oXcbES7fpTXetlNNpIcQ5j5G04HCPWNl8abCcKNUMoDjAXcvKnXNTBaDSfSZ FSZMXxEnOQP/Symz8st1KS7DbbYma/OCkrAsZ+iHC1Ozis0D28uxcdyrPZi36bkV
+JxMjjtVpU8r7sEDmQRlh4CeRqYfimNusm8WO3Yod+PLO33doUhEwBMJOu1s3+oG MKNST42uV86CTEan2yaHaynRUFNHC+bZjA+izOALtg3CyguOqrmL1LL5ID8Q1K8f
rQIDAQAB jQIDAQAB
-----END PUBLIC KEY----- -----END PUBLIC KEY-----

View File

@ -3,6 +3,10 @@ league_oauth2_server:
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%' private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%'
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%' encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
access_token_ttl: PT3H # 3 hours
refresh_token_ttl: P1M # 1 month
auth_code_ttl: PT10M # 10 minutes
require_code_challenge_for_public_clients: false
resource_server: resource_server:
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%' public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'
scopes: scopes:

View File

@ -0,0 +1,30 @@
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
allow_headers: ['Content-Type', 'Authorization']
expose_headers: ['Link']
max_age: 3600
paths:
'^/token$':
origin_regex: true
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_methods: ['POST', 'OPTIONS']
allow_credentials: true
max_age: 3600
'^/authorize$':
origin_regex: true
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_methods: ['GET', 'POST', 'OPTIONS']
allow_credentials: true
max_age: 3600
'^/login$':
origin_regex: true
allow_origin: ['*']
allow_headers: ['Content-Type', 'Authorization']
allow_methods: ['GET', 'POST', 'OPTIONS']
allow_credentials: true
max_age: 3600

View File

@ -48,6 +48,7 @@ security:
- { path: ^/login, roles: PUBLIC_ACCESS } - { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/token, roles: PUBLIC_ACCESS } - { path: ^/token, roles: PUBLIC_ACCESS }
- { path: ^/oauth2/token, roles: PUBLIC_ACCESS } - { path: ^/oauth2/token, roles: PUBLIC_ACCESS }
- { path: ^/token, roles: PUBLIC_ACCESS }
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED } - { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
- { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY } - { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/, roles: ROLE_USER } - { path: ^/, roles: ROLE_USER }

View File

@ -19,6 +19,9 @@ services:
- '../src/DependencyInjection/' - '../src/DependencyInjection/'
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
App\EventSubscriber\:
resource: '../src/EventSubscriber/'
tags: ['kernel.event_subscriber']
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones

View File

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\User; use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
class OAuth2Controller extends AbstractController class OAuth2Controller extends AbstractController
@ -17,7 +18,7 @@ class OAuth2Controller extends AbstractController
return new JsonResponse([ return new JsonResponse([
'message' => 'Authentification réussie !', 'message' => 'Authentification réussie !',
'email' => $user->getEmail(), 'email' => $user->getEmail(),
'name' => $user->getUsername(), 'name' => $user->getName()
]); ]);
} }
@ -33,7 +34,7 @@ class OAuth2Controller extends AbstractController
return new JsonResponse([ return new JsonResponse([
'sub' => $user->getId(), 'sub' => $user->getId(),
'username' => $user->getUsername(), 'username' => $user->getName(),
'email' => $user->getEmail(), 'email' => $user->getEmail(),
'roles' => $user->getRoles(), 'roles' => $user->getRoles(),
]); ]);

View File

@ -6,9 +6,18 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\HttpFoundation\Request;
use App\Service\CguUserService;
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
private CguUserService $cguUserService;
public function __construct(CguUserService $cguUserService)
{
$this->cguUserService = $cguUserService;
}
#[Route(path: '/login', name: 'app_login')] #[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {
@ -31,4 +40,23 @@ class SecurityController extends AbstractController
{ {
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
} }
#[Route(path: '/consent', name: 'app_consent')]
public function consent(Request $request): Response
{
// Handle form submission
if ($request->isMethod('POST')) {
// Check if user declined consent
if (!$request->request->has('decline')) {
// User accepted the CGU, save this in the database
$this->cguUserService->acceptLatestCgu($this->getUser());
}
// Redirect back to the OAuth authorization endpoint with all the query parameters
return $this->redirectToRoute('oauth2_authorize', $request->query->all());
}
// For GET requests, just show the consent form
return $this->render('security/consent.html.twig');
}
} }

View File

@ -11,6 +11,7 @@ use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\FirewallMapInterface; use Symfony\Component\Security\Http\FirewallMapInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait; use Symfony\Component\Security\Http\Util\TargetPathTrait;
use App\Service\CguUserService;
class AuthorizationCodeSubscriber implements EventSubscriberInterface class AuthorizationCodeSubscriber implements EventSubscriberInterface
{ {
@ -20,35 +21,42 @@ class AuthorizationCodeSubscriber implements EventSubscriberInterface
private UrlGeneratorInterface $urlGenerator; private UrlGeneratorInterface $urlGenerator;
private RequestStack $requestStack; private RequestStack $requestStack;
private $firewallName; private $firewallName;
private $cguUserService;
public function __construct(Security $security, UrlGeneratorInterface $urlGenerator, RequestStack $requestStack, FirewallMapInterface $firewallMap) public function __construct(Security $security, UrlGeneratorInterface $urlGenerator, RequestStack $requestStack, FirewallMapInterface $firewallMap, CguUserService $cguUserService)
{ {
$this->security = $security; $this->security = $security;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
$this->requestStack = $requestStack; $this->requestStack = $requestStack;
$this->firewallName = $firewallMap->getFirewallConfig($requestStack->getCurrentRequest())->getName(); $this->firewallName = $firewallMap->getFirewallConfig($requestStack->getCurrentRequest())->getName();
$this->cguUserService = $cguUserService;
} }
public function onLeagueOauth2ServerEventAuthorizationRequestResolve(AuthorizationRequestResolveEvent $event): void public function onLeagueOauth2ServerEventAuthorizationRequestResolve(AuthorizationRequestResolveEvent $event): void
{ {
$request = $this->requestStack->getCurrentRequest(); $request = $this->requestStack->getCurrentRequest();
$user = $this->security->getUser(); $user = $this->security->getUser();
$this->saveTargetPath($request->getSession(), $this->firewallName, $request->getUri());
// Default response is to redirect to login if not logged in
$response = new RedirectResponse($this->urlGenerator->generate('app_login'), 307); $response = new RedirectResponse($this->urlGenerator->generate('app_login'), 307);
if ($user instanceof UserInterface) {
//On approuve le consentement automatiquement if (!$user instanceof UserInterface) {
$event->resolveAuthorization(true); // Save the target path and redirect to login
$request->getSession()->remove('consent_granted'); $this->saveTargetPath($request->getSession(), $this->firewallName, $request->getUri());
$event->setResponse($response);
return; return;
//Decommenter et implemeter pour rediriger vers les constentement
/*if ($request->getSession()->get('consent_granted') !== null) {
$event->resolveAuthorization($request->getSession()->get('consent_granted'));
$request->getSession()->remove('consent_granted');
return;
}
$response = new RedirectResponse($this->urlGenerator->generate('app_consent', $request->query->all()), 307);*/
} }
$event->setResponse($response);
// User is logged in, check if they've accepted the latest CGU
if (!$this->cguUserService->isLatestCguAccepted($user)) {
// Redirect to consent page with all query parameters
$response = new RedirectResponse($this->urlGenerator->generate('app_consent', $request->query->all()), 307);
$event->setResponse($response);
return;
}
// User has accepted CGU, authorize the request
$event->resolveAuthorization(true);
} }
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array

View File

@ -16,6 +16,15 @@ class CguRepository extends ServiceEntityRepository
parent::__construct($registry, Cgu::class); parent::__construct($registry, Cgu::class);
} }
public function findLatestCgu(): ?Cgu
{
return $this->createQueryBuilder('c')
->orderBy('c.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
// /** // /**
// * @return Cgu[] Returns an array of Cgu objects // * @return Cgu[] Returns an array of Cgu objects
// */ // */

View File

@ -0,0 +1,53 @@
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Entity\Cgu;
use App\Entity\CguUser;
class CguUserService
{
public function __construct(private EntityManagerInterface $entityManager)
{
}
public function isLatestCguAccepted(UserInterface $user): bool
{
$latestCgu = $this->entityManager->getRepository(Cgu::class)->findLatestCgu();
if (!$latestCgu) {
// If no CGU exists, set to false
return false;
}
$cguUser = $this->entityManager->getRepository(CguUser::class)->findOneBy(['users' => $user, 'cgu' => $latestCgu]);
if (!$cguUser) {
// If the relation doesn't exist, the user hasn't seen or accepted the latest CGU
return false;
}
return $cguUser->isAccepted();
}
public function acceptLatestCgu(UserInterface $user): void
{
$latestCgu = $this->entityManager->getRepository(Cgu::class)->findLatestCgu();
if (!$latestCgu) {
// No CGU to accept
return;
}
$cguUser = $this->entityManager->getRepository(CguUser::class)->findOneBy(['users' => $user, 'cgu' => $latestCgu]);
if (!$cguUser) {
// Create a new CguUser relation if it doesn't exist
$cguUser = new CguUser();
$cguUser->setUsers($user);
$cguUser->setCgu($latestCgu);
$this->entityManager->persist($cguUser);
}
$cguUser->setIsAccepted(true);
$this->entityManager->flush();
}
}

View File

@ -39,6 +39,18 @@
"config/routes/league_oauth2_server.yaml" "config/routes/league_oauth2_server.yaml"
] ]
}, },
"nelmio/cors-bundle": {
"version": "2.5",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.5",
"ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
},
"files": [
"config/packages/nelmio_cors.yaml"
]
},
"nyholm/psr7": { "nyholm/psr7": {
"version": "1.8", "version": "1.8",
"recipe": { "recipe": {

View File

@ -63,12 +63,13 @@
<i >{{ ux_icon('bi:menu-up', {height: '20px', width: '20px'}) }}</i> <i >{{ ux_icon('bi:menu-up', {height: '20px', width: '20px'}) }}</i>
</div> </div>
<div class="col-9"> <div class="col-9">
<select class="form-control"> <a href="http://api.solutions-easy.moi"> tyet </a>
{# <select class="form-control">
<option>Exploit</options> <option>Exploit</options>
<option>Monithor</options> <option>Monithor</options>
<option>Check</options> <option>Check</options>
<option>Access</options> <option>Access</options>
</select> </select> #}
</div> </div>
</div> </div>
<a class="dropdown-item" style="padding-left: 8px;" href="#"> <a class="dropdown-item" style="padding-left: 8px;" href="#">

View File

@ -0,0 +1,36 @@
{% extends 'base.html.twig' %}
{% block title %} Consent {% endblock %}
{% block body %}
{% if app.user %}
<div class="mb-3">
<div class="container mt-4">
<h2>Data Usage Consent</h2>
<div class="card">
<div class="card-body">
<p>Dear {{ app.user.userIdentifier }},</p>
<p>We request your consent to process your personal data. By agreeing, you acknowledge that:</p>
<ul>
<li>We will store and process your personal information securely</li>
<li>Your data will only be used for the purposes specified in our privacy policy</li>
<li>You can withdraw your consent at any time</li>
</ul>
<form method="post" action="{{ path('app_consent', app.request.query.all) }}" class="mt-4">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="consent" name="consent" value="1" required>
<label class="form-check-label" for="consent">
I agree to the processing of my personal data
</label>
</div>
<button type="submit" class="btn btn-primary">Submit Consent</button>
<button type="submit" name="decline" value="1" class="btn btn-secondary" formnovalidate>Decline</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}