Skip to content

Commit

Permalink
Integration ProConnect
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois committed Feb 13, 2025
1 parent 80ace99 commit 707b061
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ S3_ENDPOINT=""

MAILER_SENDER=contact@dialog.beta.gouv.fr
MAILER_DSN=smtp://mailer:1025

PRO_CONNECT_CLIENT_ID=votre_client_id
PRO_CONNECT_CLIENT_SECRET=votre_client_secret
5 changes: 5 additions & 0 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ security:
providers:
user:
id: App\Infrastructure\Security\Provider\UserProvider

firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
Expand All @@ -20,6 +21,10 @@ security:
password_parameter: password
logout:
path: app_logout
# ProConnect
custom_authenticator:
- App\Infrastructure\Security\ProConnectAuthenticator

access_control:
- { path: ^/admin, roles: ROLE_SUPER_ADMIN }
- { path: '^/regulations/([0-9a-f]{8}-[0-9a-f]{4}-[13-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$', methods: [GET], roles: PUBLIC_ACCESS }
Expand Down
2 changes: 2 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ services:
$metabaseSiteUrl: '%env(APP_METABASE_SITE_URL)%'
$metabaseSecretKey: '%env(APP_METABASE_SECRET_KEY)%'
$mediaLocation: '%env(APP_MEDIA_LOCATION)%'
$proConnectClientId: '%env(PRO_CONNECT_CLIENT_ID)%'
$proConnectClientSecret: '%env(PRO_CONNECT_CLIENT_SECRET)%'

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\ProConnect;

use App\Application\CommandInterface;

final readonly class CreateProConnectUserCommand implements CommandInterface
{
public function __construct(
public string $email,
public array $userInfo,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace App\Application\User\Command\ProConnect;

use App\Application\ApiOrganizationFetcherInterface;
use App\Application\DateUtilsInterface;
use App\Application\IdFactoryInterface;
use App\Domain\User\Enum\OrganizationRolesEnum;
use App\Domain\User\Enum\UserRolesEnum;
use App\Domain\User\Exception\OrganizationNotFoundException;
use App\Domain\User\Organization;
use App\Domain\User\OrganizationUser;
use App\Domain\User\ProConnectUser;
use App\Domain\User\Repository\OrganizationRepositoryInterface;
use App\Domain\User\Repository\OrganizationUserRepositoryInterface;
use App\Domain\User\Repository\ProConnectUserRepositoryInterface;
use App\Domain\User\Repository\UserRepositoryInterface;
use App\Domain\User\User;

final class CreateProConnectUserCommandHandler
{
public function __construct(
private IdFactoryInterface $idFactory,
private UserRepositoryInterface $userRepository,
private ProConnectUserRepositoryInterface $proConnectUserRepository,
private OrganizationUserRepositoryInterface $organizationUserRepository,
private OrganizationRepositoryInterface $organizationRepository,
private DateUtilsInterface $dateUtils,
private ApiOrganizationFetcherInterface $organizationFetcher,
) {
}

public function __invoke(CreateProConnectUserCommand $command): User
{
['given_name' => $givenName, 'family_name' => $familyName, 'organization_siret' => $siret] = $command->userInfo;

$user = $this->userRepository->findOneByEmail($command->email);
if ($user instanceof User) {
return $user;
}

$organization = $this->organizationRepository->findOneBySiret($siret);
$organizationRole = OrganizationRolesEnum::ROLE_ORGA_CONTRIBUTOR->value; // Default organization role
$now = $this->dateUtils->getNow();

if (!$organization) {
try {
['name' => $name] = $this->organizationFetcher->findBySiret($siret);
} catch (OrganizationNotFoundException $e) {
throw $e;
}

$organizationRole = OrganizationRolesEnum::ROLE_ORGA_ADMIN->value; // The first user in an organization becomes an admin
$organization = (new Organization($this->idFactory->make()))
->setCreatedAt($now)
->setSiret($siret)
->setName($name);
$this->organizationRepository->add($organization);
}

$user = (new User($this->idFactory->make()))
->setFullName(\sprintf('%s %s', $givenName, $familyName))
->setEmail($command->email)
->setRoles([UserRolesEnum::ROLE_USER->value])
->setRegistrationDate($now)
->setVerified();

$proConnectUser = new ProConnectUser(
uuid: $this->idFactory->make(),
user: $user,
);

$user->setProConnectUser($proConnectUser);

$organizationUser = (new OrganizationUser($this->idFactory->make()))
->setUser($user)
->setOrganization($organization)
->setRoles($organizationRole);

$this->userRepository->add($user);
$this->proConnectUserRepository->add($proConnectUser);
$this->organizationUserRepository->add($organizationUser);

return $user;
}
}
12 changes: 12 additions & 0 deletions src/Domain/User/Repository/ProConnectUserRepositoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Domain\User\Repository;

use App\Domain\User\ProConnectUser;

interface ProConnectUserRepositoryInterface
{
public function add(ProConnectUser $proConnectUser): ProConnectUser;
}
7 changes: 7 additions & 0 deletions src/Domain/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ public function setPasswordUser(PasswordUser $passwordUser): self
return $this;
}

public function setProConnectUser(ProConnectUser $proConnectUser): self
{
$this->proConnectUser = $proConnectUser;

return $this;
}

public function getPasswordUser(): ?PasswordUser
{
return $this->passwordUser;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Controller\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

final class ProConnectLoginController
{
public function __construct(
private \Twig\Environment $twig,

Check failure on line 15 in src/Infrastructure/Controller/Security/ProConnectLoginController.php

View workflow job for this annotation

GitHub Actions / build

Property App\Infrastructure\Controller\Security\ProConnectLoginController::$twig is never read, only written.
private AuthenticationUtils $authenticationUtils,

Check failure on line 16 in src/Infrastructure/Controller/Security/ProConnectLoginController.php

View workflow job for this annotation

GitHub Actions / build

Property App\Infrastructure\Controller\Security\ProConnectLoginController::$authenticationUtils is never read, only written.
private UrlGeneratorInterface $urlGenerator,
private string $proConnectClientId,
) {
}

#[Route('/proconnect/auth', name: 'pro_connect_start')]
public function proConnectLogin(): RedirectResponse
{
$params = [
'response_type' => 'code',
'client_id' => $this->proConnectClientId,
'redirect_uri' => $this->urlGenerator->generate('pro_connect_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
'scope' => 'openid email profile organization',
'state' => bin2hex(random_bytes(10)),
];

return new RedirectResponse(
'https://auth.entreprise.api.gouv.fr/authorize?' . http_build_query($params),
);
}

#[Route('/proconnect/auth/callback', name: 'pro_connect_callback')]
public function proConnectCallback()
{
// Géré par l'authenticator
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Persistence\Doctrine\Repository\User;

use App\Domain\User\ProConnectUser;
use App\Domain\User\Repository\ProConnectUserRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

final class ProConnectUserRepository extends ServiceEntityRepository implements ProConnectUserRepositoryInterface
{
public function __construct(
ManagerRegistry $registry,
) {
parent::__construct($registry, ProConnectUser::class);
}

public function add(ProConnectUser $proConnectUser): ProConnectUser
{
$this->getEntityManager()->persist($proConnectUser);

return $proConnectUser;
}
}
85 changes: 85 additions & 0 deletions src/Infrastructure/Security/ProConnectAuthenticator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Security;

use App\Application\CommandBusInterface;
use App\Application\User\Command\ProConnect\CreateProConnectUserCommand;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
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\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class ProConnectAuthenticator extends AbstractAuthenticator
{
public function __construct(
private HttpClientInterface $httpClient,
private UrlGeneratorInterface $urlGenerator,
private CommandBusInterface $commandBus,
private string $proConnectClientId,
private string $proConnectClientSecret,
) {
}

public function supports(Request $request): ?bool
{
return $request->attributes->get('_route') === 'pro_connect_callback';
}

public function authenticate(Request $request): Passport
{
$code = $request->query->get('code');

// Récupération du token
$response = $this->httpClient->request('POST', 'https://auth.entreprise.api.gouv.fr/token', [
'body' => [
'grant_type' => 'authorization_code',
'code' => $code,
'client_id' => $this->proConnectClientId,
'client_secret' => $this->proConnectClientSecret,
'redirect_uri' => $this->urlGenerator->generate('pro_connect_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
],
]);

$token = $response->toArray()['access_token'];

// Récupération des infos utilisateur
$userInfo = $this->httpClient->request('GET', 'https://auth.entreprise.api.gouv.fr/userinfo', [
'headers' => ['Authorization' => 'Bearer ' . $token],
])->toArray();

return new SelfValidatingPassport(
new UserBadge($userInfo['email'], function (string $email) use ($userInfo) {
return $this->commandBus->handle(
new CreateProConnectUserCommand(
$email,
$userInfo,
),
);
}),
);
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse($this->urlGenerator->generate('app_landing'));
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// Redirection en cas d'échec avec message d'erreur
return new RedirectResponse(
$this->urlGenerator->generate('app_login', [
'error' => $exception->getMessageKey(),
]),
);
}
}

0 comments on commit 707b061

Please sign in to comment.