set up doc for API
This commit is contained in:
parent
c0a8a9ab82
commit
b2bb9fc78b
|
|
@ -0,0 +1,194 @@
|
|||
# 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
|
||||
```yaml
|
||||
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
|
||||
```yaml
|
||||
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
|
||||
```
|
||||
```yaml
|
||||
# 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:
|
||||
```php
|
||||
#[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
|
||||
<?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
|
||||
<?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
|
||||
```dotenv
|
||||
SSO_URL='http://portail.solutions-easy.moi'
|
||||
```
|
||||
```yaml
|
||||
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.
|
||||
|
||||
```cmd
|
||||
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
|
||||
```dotenv
|
||||
OAUTH_SSO_IDENTIFIER='sso-own-identifier'
|
||||
```
|
||||
and we are smart so what do we do? we add it to the services.yaml
|
||||
```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 )
|
||||
|
|
@ -320,8 +320,10 @@ 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
|
||||
The recreate the client and enter the scopes and grant types after creating the client directly in the db
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue