# 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: } use_state: false ``` ### .env ```dotenv # .env # CORS CORS_ALLOW_ORIGIN=http://*.your domain/*' # OAUTH2 OAUTH2_CLIENT_ID= OAUTH2_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 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 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 --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 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 ```