Easy_solution/docs/API.md

8.0 KiB

Intro

The Api made some changes to the current structure of the project. These changes include the following:

  • Security : the security.yaml file has been updated both on the server and the client side
  • Controllers: Controllers need to be updated on the client side
  • Services: Services and token management need updates on the client side
  • Health: I now want to commit a Talon E over a wall IRL
  • Entities: Entities need to be updated on the client side to include a new field (sso_id).
    • This field will be used a lot, so add it to an entity if you are going to work with the API and the SSO. PLEASE.
  • Roles: A new role was added because we are doing M2M. Only God know how that work now, but it works, so I might start praying from now on.

Security

new firewall was added. Keep the same structure for future firewalls.

Client side

        api_project:
          pattern: ^/api/v1/project #ofc, this is an example, please THINK and change the name
          stateless: true
          access_token:
            token_handler: App\Security\SsoTokenHandler

Same thing, new firewall was added.

Server side

    api_token_validation:
        pattern: ^/api/validate-token #this is NOT an example. DON'T change or it will all go to sh.t
        stateless: true
        oauth2: true
# A rajouter dans l'access_control aussi !!! IMPORTANT !!!
          - { path: ^/api/validate-token, roles: PUBLIC_ACCESS }

Controllers

On the client side, create a new controller for the API. This controller need will work in a REST manner. The route should be as follows:

#[Route('/api/v1/project', name: 'api_project')] //ofc, this is an example, please THINK and change the name

Here is a full example of a controller with the create method.

<?php
#[Route('/api/v1/project', name: 'api_project_')]
#[IsGranted('ROLE_API_INTERNAL')]
class ProjectApi extends AbstractController{

    public function __construct(
        private readonly EntityManagerInterface       $entityManager)
        {
    }

    #[Route('/create', name: 'create', methods: ['POST'])]
    public function createProject(Request $request): JSONResponse
    {
        $data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
        $projet = new Projet();

            $entity = $this->entityManager->getRepository(Entity::class)->find($data['entity_id']);
            $precision= $data['timestamp'];
            $validPrecisions = array_map(fn($case) => $case->value, TimestampPrecision::cases());
        if (!in_array($precision, $validPrecisions, true)) {
            return $this->json(['success' => false, 'message' => 'Précision d\'horodatage invalide.'], 400);
        }

        try {
            $timestampPrecision = TimestampPrecision::from($precision);
            $projet->setTimestampPrecision($timestampPrecision);
            $projet->setProjet($data['projet']);
            $projet->setEntityId($entity);
            $projet->setBdd($data['bdd']);
            $projet->setIsactive($data['isactive']);
            $projet->setLogo($data['logo']);
            $projet->setDeletionAllowed($data['deletion']);
            $projet->setSsoId($data['id']); // c'est l'id du projet dans le portail, pas la bdd local

            $this->entityManager->persist($projet);
            $this->entityManager->flush();
            return new JsonResponse(['message' => 'Project created successfully', 'project_id' => $projet->getId()], 201);
        }catch ( \Exception $e){
            return new JsonResponse(['error' => 'Failed to create project: ' . $e->getMessage()], 500);
        }
    }

Services

So, now we are getting into the thick of it. We are just COOK 🍗. We implement a new pretty service called SsoTokenHandler. This is used to get the token received from the portal request, and validate it. It is validaded by doing a call back to the SSO and asking if the token is valid. ( we create a new token for the SSO so it handles M2M)

<?php

// src/Security/SsoTokenHandler.php
namespace App\Security;

use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class SsoTokenHandler implements AccessTokenHandlerInterface
{
    public function __construct(
        private HttpClientInterface $httpClient,
        private string $ssoUrl
    ) {}

    public function getUserBadgeFrom(string $accessToken): UserBadge
    {
        // 1. Call the SSO to validate the token
        // Note: You need an endpoint on your SSO that returns 200 for M2M tokens
        try {
        $response = $this->httpClient->request('GET', $this->ssoUrl . '/api/validate-token', [
            'headers' => ['Authorization' => 'Bearer ' . $accessToken]
        ]);

        // If the SSO redirects, HttpClient might follow it to the login page.
        // Let's see the first response code.
        if ($response->getStatusCode() !== 200) {
            // Log the content to see the HTML "Login" page that is causing the JSON error
            // dump($response->getContent(false));
            throw new BadCredentialsException();
        }
    } catch (\Exception $e) {
        // This will show you if you are hitting a 302 Redirect
        throw new BadCredentialsException('SSO returned invalid response: ' . $e->getMessage());
    }

        if (200 !== $response->getStatusCode()) {
            throw new BadCredentialsException('Invalid SSO Token');
        }

        $data = $response->toArray();

        // 2. Identify if it's a User or a Machine
        $identifier = $data['email'] ?? 'SYSTEM_SSO_SERVER';

        // 3. Return the badge with a "loader" closure
        return new UserBadge($identifier, function($userIdentifier) use ($data) {
            // If it's the SSO server calling, give it a specific role
            if ($userIdentifier === 'SYSTEM_SSO_SERVER') {
                return new InMemoryUser($userIdentifier, null, ['ROLE_API_INTERNAL']);
            }

            // Otherwise, let the normal user provider handle it (for standard users)
            // You might need to inject your actual UserProvider here if needed
            return new InMemoryUser($userIdentifier, null, ['ROLE_USER']);
        });
    }
}

Important note 1

we need to add the portal url to the .env and declare it as a parameter in services.yaml

SSO_URL='http://portail.solutions-easy.moi'
parameters:
    sso_url: '%env(SSO_URL)%'

    App\Security\SsoTokenHandler:
      arguments:
        $ssoUrl: '%sso_url%'

Server side

On the server side, we need to create a new client, which will be himself, same as earlier, create it, and boom we are good. The validate route is already created, so dw abt it.

php bin/console league:oauth2-server:create-client sso_internal_service --grant-type "client_credentials"

now, copy the identifier, and paste it in the .env file

OAUTH_SSO_IDENTIFIER='sso-own-identifier'

and we are smart so what do we do? we add it to the services.yaml

parameters:
    oauth_sso_identifier: '%env(OAUTH_SSO_IDENTIFIER)%'
    
    App\Service\SSO\ProjectService:
        arguments:
            $appUrl: '%app_url%'
            $clientIdentifier: '%oauth_sso_identifier%'

We should be good now ( I hope ). Open the portal, try your call and check if it works, if it doesn't, check the logs and debug, you are a dev for a reason, so use your brain and debug. If it still doesn't work, start praying, because I have no idea what to do anymore, but it works on my side, so it should work on yours, if not, well, I don't know what to say. Good luck. Jokes aside, bugs often come from security problem, if the client returns a 401 error, it can be for multiple reasons and not necessarily because of the token but maybe because of the token validation. Another commun bug is mismatching of the data you send, so double check. GLHF ( you won't have fun, but good luck anyway )