13 KiB
13 KiB
Client setup
Add needed dependencies
composer require nelmio/cors-bundle
composer require knpuniversity/oauth2-client-bundle
Configure the bundle
nelmio/cors-bundle
# config/packages/nelmio_cors.yaml
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
knpuniversity/oauth2-client-bundle
# config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
clients:
# configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
sudalys:
type: generic
provider_class: Sudalys\OAuth2\Client\Provider\Sudalys
client_id: '%env(OAUTH2_CLIENT_ID)%'
client_secret: '%env(OAUTH2_CLIENT_SECRET)%'
redirect_route: uri # The route to redirect to after authentication (must match the one in the server DB uri DB)
provider_options: {
domain: <link to domain>
}
use_state: false
.env
# .env
# CORS
CORS_ALLOW_ORIGIN=http://*.your domain/*'
# OAUTH2
OAUTH2_CLIENT_ID=<client_id>
OAUTH2_CLIENT_SECRET=<client_secret>
Copy and paste the client library then modify the conposer.json autoloard directive to include the new library
"autoload": {
"psr-4": {
"App\\": "src/",
"Sudalys\\OAuth2\\Client\\": "libs/sudalys/oauth2-client/src"
}
},
<?php
/** @var string */
public $domain = 'link to SSO portal ';
Copy and paste the SSOAuthenticator class modify the target url to match the route in server DB and redirect route
SsoAuthenticator.php
<?php
namespace App\Security;
use App\Entity\User;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
/**
* Class SudalysSSoAuthenticator
*/
class SsoAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
private $clientRegistry;
private $em;
private $router;
private $urlGenerator;
use TargetPathTrait;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router, UrlGeneratorInterface $urlGenerator)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
$this->urlGenerator = $urlGenerator;
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
// Use the KnpU client to generate the correct authorization URL,
// including state / redirect_uri / scope / pkce as configured.
$client = $this->getSudalysClient();
// Option A: let the client use the configured redirect uri and default scopes:
return $client->redirect();
// Option B (explicit): specify scopes and an explicit redirect_uri (absolute URL)
// $redirectUri = $this->urlGenerator->generate('sudalys_check', [], UrlGeneratorInterface::ABSOLUTE_URL);
// return $client->redirect(['openid', 'profile'], ['redirect_uri' => $redirectUri]);
}
public function supports(Request $request): ?bool
{
// If your OAuth redirect route is named 'sudalys_check', check by route:
if ($request->attributes->get('_route') === 'sudalys_check') {
return true;
}
// fallback: also support requests containing the authorization code
return (bool) $request->query->get('code');
}
public function authenticate(Request $request): Passport
{
$client = $this->getSudalysClient();
$accessToken = $this->fetchAccessToken($client);
$session = $request->getSession();
$session->set('access_token', $accessToken->getToken());
// Stocker également le refresh token s'il est disponible
if ($accessToken->getRefreshToken()) {
$session->set('refresh_token', $accessToken->getRefreshToken());
}
return new SelfValidatingPassport(
new UserBadge($accessToken->getToken(), function () use ($accessToken, $client) {
//show in log the access token
$sudalysSsoUser = $client->fetchUserFromToken($accessToken);
$ssoId = $sudalysSsoUser->getId();
/*
* On regarde si le token est valide
*/
if($accessToken->getExpires() > time()) {
// Token valide, on regarde si l'utilisateur existe en bdd locale
/** @var User $userInDatabase */
$user = $this->em->getRepository(User::class)->findOneBy(['ssoId' => $ssoId]);
/**
* on cree l'utilisateur s'il n'existe pas
**/
if (!$user) {
$user = new User();
$user->setEmail($ssoData->getEmail());
$user->setPrenom($ssoData->getName());
$user->setNom($ssoData->getSurname());
$user->setSsoId($ssoData->getId());
$this->em->persist($user);
}else{
// On met a jour l'utilisateur
$user->setEmail($ssoData->getEmail());
$user->setPrenom($ssoData->getName());
$user->setNom($ssoData->getSurname());
$this->em->persist($user);
}
//handle UOs links
$ssoArray = $ssoData->toArray();
$uoData = $ssoArray['uos'] ?? [];
foreach ($uoData as $uo) {
$ssoOrgId = $uo['id'];
$entity = $this->em->getRepository(Entity::class)->findOneBy(['ssoId' => $ssoOrgId]);
if (!$entity) {
$entity = new Entity();
$entity->setSsoId($ssoOrgId);
$entity->setNom($uo['name']);
$this->em->persist($entity);
}
$role = $this->em->getRepository(Roles::class)->findOneBy(['name' => $uo['role']]);
// Check if the user-organization link already exists
$existingLink = $this->em->getRepository(UsersOrganizations::class)->findOneBy([
'users' => $user,
'organizations' => $entity
]);
if (!$existingLink) {
// Create a new link if it doesn't exist
$newLink = new UsersOrganizations();
$newLink->setUsers($user);
$newLink->setOrganizations($entity);
$newLink->setRole($role);
$this->em->persist($newLink);
} else {
// Update the role if the link already exists
$existingLink->setRole($role);
$existingLink->setModifiedAt(new \DateTimeImmutable());
$this->em->persist($existingLink);
}
}
$this->em->flush();
return $user;
}
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('app_index');
return new RedirectResponse($targetUrl);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
/**
*
*/
private function getSudalysClient()
{
return $this->clientRegistry->getClient('sudalys');
}
}
namespace App\Security\SsoAuthenticator;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// change "app_homepage" to some route in your app
$targetUrl = $this->router->generate('your redirect route');
return new RedirectResponse($targetUrl);
}
Security.yaml
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
main:
lazy: true
provider: app_user_provider
custom_authenticators:
- App\Security\SsoAuthenticator
entry_point: App\Security\SsoAuthenticator
logout:
path: app_logout
target: app_after_logout
invalidate_session: true
delete_cookies: ['PHPSESSID']
access_control:
- { path: ^/sso/login, roles: PUBLIC_ACCESS }
- { path: ^/sso/check, roles: PUBLIC_ACCESS }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
Setup oauth controller
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
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\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class SsoController extends AbstractController
{
#[Route('/sso/login', name: 'sudalys_sso_login')]
public function login(ClientRegistry $clientRegistry): RedirectResponse
{
return $clientRegistry->getClient('sudalys')->redirect();
}
#[Route('/sso/check', name: 'sudalys_sso_check')]
public function connectCheckAction(Request $request)
{
return $this->redirectToRoute('app_index');
}
#[Route('/logout', name: 'app_logout')]
public function logout(): void
{
throw new \Exception('This should never be reached!');
}
#[Route('/logout-redirect', name: 'app_after_logout')]
public function afterLogout(): RedirectResponse
{
// SSO logout URL — adjust if necessary
$ssoLogout = 'http://portail.solutions-easy.moi/sso_logout';
return new RedirectResponse($ssoLogout);
}
}
Server setup
Create OAuth2 client
php bin/console league:oauth2-server:create-client <name> --redirect-uri="http://your-client-domain/sso/check" --scope="openid" --scope="profile" --scope="email" --grant-type=authorization_code
If there is a scope or grand error, delete the client do the following first
php bin/console league:oauth2-server:delete-client <identifier>
Identifier can be found in the database oauth2_client table To recreate the client and enter the scopes and grant types after creating the client directly in the db
scopes = email profile openid
grants = authorization_code