366 lines
13 KiB
Markdown
366 lines
13 KiB
Markdown
# Client setup
|
|
## Add needed dependencies
|
|
```bash
|
|
composer require nelmio/cors-bundle
|
|
composer require knpuniversity/oauth2-client-bundle
|
|
```
|
|
|
|
## Configure the bundle
|
|
### nelmio/cors-bundle
|
|
```yaml
|
|
# 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
|
|
```yaml
|
|
# 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
|
|
```dotenv
|
|
# .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
|
|
```json
|
|
"autoload": {
|
|
"psr-4": {
|
|
"App\\": "src/",
|
|
"Sudalys\\OAuth2\\Client\\": "libs/sudalys/oauth2-client/src"
|
|
}
|
|
},
|
|
```
|
|
|
|
```php
|
|
<?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
|
|
<?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');
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```php
|
|
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
|
|
```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
|
|
<?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
|
|
```cmd
|
|
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
|
|
```cmd
|
|
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
|
|
```text
|
|
scopes = email profile openid
|
|
grants = authorization_code
|
|
```
|
|
|
|
|