Easy_solution/docs/Client_Setup.md

11 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($sudalysSsoUser->getEmail());
                        $user->setName($sudalysSsoUser->getName());
                        $user->setSurname($sudalysSsoUser->getSurname());
                        $user->setSsoId($sudalysSsoUser->getId());
                        $this->em->persist($user);
                    }else{
                        // On met a jour l'utilisateur
                        $user->setEmail($sudalysSsoUser->getEmail());
                        $user->setName($sudalysSsoUser->getName());
                        $user->setSurname($sudalysSsoUser->getSurname());
                        $this->em->persist($user);
                    }
                    $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