Skip to content

Commit

Permalink
Merge pull request #1860 from jolelievre/admin-api-doc
Browse files Browse the repository at this point in the history
  • Loading branch information
kpodemski authored Sep 18, 2024
2 parents 2e5f1a8 + dc0dba6 commit 0b42d68
Show file tree
Hide file tree
Showing 29 changed files with 1,573 additions and 12 deletions.
20 changes: 20 additions & 0 deletions admin-api/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Admin API
menuTitle: Admin API
chapter: true
pre: "<b>7. </b>"
weight: 7
showOnHomepage: true
---

### Chapter 7

# Admin API in PrestaShop 9

The Admin API in PrestaShop 9 is based on the popular [API Platform](https://api-platform.com/) in version 3 and fully takes advantage of its benefits.

The new API utilizes the OAuth authorization protocol and [CQRS commands]({{< relref "../development/architecture/domain/references" >}}) for its endpoints. CQRS-based endpoints are more domain-oriented and enhance business logic management compared to traditional web services connected to ObjectModel. This approach allows us to better separate concerns, align API operations closely with business requirements, and improve maintainability and scalability.

## Learn more

{{% children /%}}
66 changes: 66 additions & 0 deletions admin-api/authorization_server/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Authorization Server
menuTitle: Authorization Server
weight: 4
---

# Authorization Server

The Admin API is based on the OAuth2 protocol. PrestaShop is always the resource server in charge of providing access to the resources.

However, it can interact with either an external authorization server or act as the authorization server itself (you can even combine both features if you need to).

### Multiple authorization servers

To handle multiple authorization servers we provide an interface `PrestaShop\PrestaShop\Core\Security\OAuth2\AuthorisationServerInterface`:

```php
namespace PrestaShop\PrestaShop\Core\Security\OAuth2;

use Symfony\Component\HttpFoundation\Request;

/**
* To integrate an authorization server you must implement this interface, the methods are bridges that call the authorization server
* to ensure the access token is valid, if so it can return a representation of the user using the JwtTokenUser DTO.
*/
interface AuthorisationServerInterface
{
/**
* For each request received, the resource server loops through all the available servers implementing this interface
* and uses this method to detect which one matches with the provided access token. Your implementation of each authorization
* server must be able to recognize an access token it created, usually relying on the issuer saved in the metadata included
* in the JWT token (no convention is forced, each authorization server may store this info differently as long as it can
* recognize itself).
*
* @param Request $request
* @return bool
*/
public function isTokenValid(Request $request): bool;

/**
* If the token is valid, the authorization server must return a representation of the user using the
* JwtTokenUser DTO that contains:
* - userId: Usually, the Client ID
* - scopes: List of scopes authorized in the access token
* - issuer: An identifier for the authorization server that issued the token:
* - for external authorization servers: usually the address of the server
* - for our internal authorization server: null (it is the only allowed to use null as an issuer)
*
* @param Request $request
* @return JwtTokenUser|null
*/
public function getJwtTokenUser(Request $request): ?JwtTokenUser;
}
```

Internally we have a service `PrestaShopBundle\Security\OAuth2\PrestashopAuthorisationServer` that implements this interface relying on the data in database, but you can add other implementations for external authorization servers in modules.

The Admin API is protected by the `PrestaShop\PrestaShop\Core\Security\OAuth2\TokenAuthenticator`, each request to the API goes through this authenticator service that loops through all the detected implementation of authorization servers, it uses the `isTokenValid` method to detect which one matches with the provided access token, when it finds one it then uses `getJwtTokenUser` which represents the "logged" API client.

Every API client using the API are stored in database:
- for internal clients, they are already in the database and their external issuer is `null`
- external clients can be added based on the `JwtTokenUser` DTO, even if their Client ID may be duplicated with an internal one their external issuer must be defined, so we can always differentiate the clients

## Possible authorization servers

{{% children /%}}
174 changes: 174 additions & 0 deletions admin-api/authorization_server/external_authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
title: External authorization server
menuTitle: External authorization server
weight: 2
---

# External authorization server

You can use an external authorization server to access your resources, to do that you need to install a module that implements our `PrestaShop\PrestaShop\Core\Security\OAuth2\AuthorisationServerInterface` interface.

As an example, we developed a [keycloak_connector_demo module](https://github.com/PrestaShop/keycloak_connector_demo) based on the [Keycloak authorization server](https://www.keycloak.org/).

This module includes a [Docker](https://www.docker.com/) configuration that allows you to launch a Keycloak server locally for development purposes. It includes:
- Keycloak authorization server and its admin interface
- One default client
- Some default scopes matching the ones from the core

{{% notice note %}}
Thanks to this implementation, any authorization server can interact with and authorize the Admin API. However, both need a common contract based on the scopes defined on each endpoint of the resource server. So, you will need to provide your authorization server with a list of scopes mapping the ones used by the endpoints.
{{% /notice %}}

## Keycloak server initialization

To start the Docker container, run this command from the root folder of the module:

```bash
docker compose up
# OR if you want Keycloak to keep running in the background
docker compose up -d
```

You will then have access to the server administration via `http://localhost:8003` where you will find a realm named `prestashop`
User: `admin`
Password: `admin`

## Module installation and configuration

Clone the module repository into your `modules` folder, then go to the Module manager in your back office and install the `Keycloak OAuth2 connector demo` module.

Then go to the configuration and provide the two needed URLs for configuration:

- **Keycloak realm endpoint**: The URL on which your server can access the Keycloak realm endpoint, this is the base realm URL from which the connector can fetch the certificates and validate if the JWT Token is valid.
- **Keycloak allowed issuers**: A list of issuer values that are considered valid, usually it's the same as the realm endpoint, but in case you request your access token from different URLs, you can specify multiple endpoints, so the validation process accepts multiple issuers.

If you're using the included Docker locally, then the default configuration should be enough:

![Keycloak configuration](../img/keycloak_configuration.png)

{{% notice note %}}
For security reasons, the values in this form are encrypted in the database.
{{% /notice %}}

## Authorization server implementation

You can see the [exact implementation of the authorization server](https://github.com/PrestaShop/keycloak_connector_demo/blob/v1.1.0/src/OAuth2/KeycloakAuthorizationServer.php) in the module itself, but here is an explanation about what each method does:

```php
namespace PrestaShop\Module\KeycloakConnectorDemo\OAuth2;

class KeycloakAuthorizationServer implements AuthorisationServerInterface
{
public function isTokenValid(Request $request): bool
{
// Parses the JWT Token and check if it's valid
// Fetch the list of allowed issuers from the configuration, if the Token issuer matches one of the allowed ones
// Fetch the URL realm from the configuration (ex: http://localhost:8003/realms/prestashop)
// Download the certificates from the authorization server (ex: http://localhost:8003/realms/prestashop/protocol/openid-connect/certs)
// Check if the JWT token was correctly signed based on the public certificate
// If all these checks passed then the token is valid and issued by Keycloak and the method returns true

// Any failure along this test the method returns false
}

public function getJwtTokenUser(Request $request): ?JwtTokenUser
{
// Parses the JWT token from the request, it should contain these claims
// - clientId: The used client ID to get the access token
// - scope: a list of scope separated by spaces
// - iss: the issuer that granted the access token (ex: http://localhost:8003/realms/prestashop)
// With these three data the method can return a JwtTokenUser DTO
}
}
```

Once your service is implemented, you need to define in your module's services that PrestaShop will automatically scan and detect it. You should rely on `autoconfigure` and `autowire` to make sure your implementation is correctly identified:

```yaml
services:
PrestaShop\Module\KeycloakConnectorDemo\OAuth2\KeycloakAuthorizationServer:
# Autoconfigure to get tag from interface and to be injected in TokenAuthenticator
autoconfigure: true
autowire: true
arguments:
$client: '@prestashop.module.keycloak_connector_demo.client'
$phpEncryption: '@prestashop.module.keycloak_connector_demo.php_encrypt'
```
Now your implementation will be used by the `TokenAuthenticator` in the loop that checks for authorization servers in each request of the resource server.

## Getting an access token

The `prestashop` realm includes a client already configured, you can get an access token via this endpoint http://localhost:8003/realms/prestashop/protocol/openid-connect/token with following credentials (use Form URL encoded request):
- **grant_type**: `client_credentials`
- **client_id**: `prestashop-keycloak`
- **client_secret**: `O2kKN0fprCK2HWP6PS6reVbZThWf5LFw`

The included scopes are:
- `api_client_read`
- `api_client_write`
- `product_read`
- `product_write`

Example request using curl:

```bash
curl --request POST \
--url http://localhost:8003/realms/prestashop/protocol/openid-connect/token \
--data grant_type=client_credentials \
--data client_id=prestashop-keycloak \
--data client_secret=O2kKN0fprCK2HWP6PS6reVbZThWf5LFw \
--data 'scope=api_client_read api_client_write'
```

Example request using Insomnium:

{{< figure src="../img/insomnium_access_token.png" title="Screenshot from Insomnium where we retrieve access token for our client" >}}

You can use the [JWT.io](https://jwt.io/) to parse your JWT token and see its content, you will see that the Keycloak payload has this structure:

```json
{
"exp": 1725577479,
"iat": 1725577179,
"jti": "d939d2e1-edca-4ecc-b649-76131dc3c528",
"iss": "http://localhost:8003/realms/prestashop",
"aud": "account",
"sub": "f1bda963-3316-4a4e-84ee-1dbe20b78a0a",
"typ": "Bearer",
"azp": "prestashop-keycloak",
"acr": "1",
"allowed-origins": [
"/*"
],
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"default-roles-prestashop"
]
},
"resource_access": {
"prestashop-keycloak": {
"roles": [
"uma_protection"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "api_client_read profile api_client_write email",
"clientHost": "192.168.207.1",
"email_verified": false,
"preferred_username": "service-account-prestashop-keycloak",
"clientAddress": "192.168.207.1",
"client_id": "prestashop-keycloak"
}
```

In this payload you can see the three parameters we mentioned earlier `client_id`, `scope` and `iss`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions admin-api/authorization_server/internal_authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: Internal authorization server
menuTitle: Internal authorization server
weight: 1
---

# Internal authorization server

PrestaShop can be used as an authorization server, as such it can handle the permissions and authentication to access the resource server.

When PrestaShop is used as the authorization server, it doesn't need to perform an external call to validate the access token since it already has all the required data to do so in its database, but it still performs the same role.

## Create API client

Let's create our first client. You can add as many clients to the API as you want. In the real-world scenario, you probably want to create a separate client for each service you want to integrate. You will probably have clients like "My ERP integration", "Some marketing automation tool client", etc.

To add a new API client, navigate to Advanced Parameters -> Admin API page. Here you can see a list of all API clients, you can add a new one by clicking the "Add new API Client" button.

![PrestaShop 9 API Client](../img/api_add_new_client1.jpeg)

If you want to add a new client, you must provide information about it. In the form below, you must provide all the necessary information about the client. After that, you can click the "Generate client secret and save" button. This will generate a client secret for you and save the client in the database. **It's important to save the client secret because you won't be able to see it again.**

![PrestaShop 9 Form for adding a new API client](../img/api_add_new_client2.jpeg)

* **Client name** - the name of the client: it can be anything you want. It's just for your reference.
* **Client ID** - the ID of the client, which is a unique identifier for the client. It should be written without spaces or special characters.
* **Description** - a description of the client: it can be anything you want. It's just for your reference.
* **Lifetime** - the lifetime of the client. It's the time after which the client access token will expire. In seconds.
* **Enabled** - if the client is enabled or not. If it's not enabled, the client won't be able to use the API.
* **Scopes** - the scopes the client can use. You can enable multiple scopes. Each time you add a new scope, you will have to adjust the client's settings.

{{% notice %}}
As part of API extensibility, you can add new scopes to the API. This will allow you to create more granular permissions for your clients. You can find an [example module here](https://github.com/PrestaShop/example-modules/tree/master/api_module) and in [ps_apiresources](https://github.com/PrestaShop/ps_apiresources) repository.
{{% /notice %}}

Once again: after you save the client, you will see the client secret, but you won't be able to see it again. You can only see it once when you generate it. It's important to save it because you need it to authenticate your client.

{{< figure src="../img/api_add_new_client3.jpeg" title="Remember to copy secret for your API Client" >}}

## Getting an access token

Now that you created your first API client, you can perform connection to the API. You can use Postman or any other tool to make HTTP requests.

The first thing we are going to do is get the access token. You need to send a POST request to the `/admin-api/access_token` endpoint to do that. You need to provide the following parameters:

* **client_id** - the ID of the client you created
* **client_secret** - the secret of the client you created
* **grant_type** - the type of grant you want to use. In this case, it's `client_credentials`
* **scope** - the scopes you want to use. You can provide multiple scopes. In this case, we are going to use `api_client_read`, `api_client_write`, `customer_group_read`, `customer_group_write`.

{{% notice %}}
You have many tools to debug APIs. You can use Postman, [curl](https://curl.se/), or any other tool (like [Insomnia](https://insomnia.rest/)) that allows you to make HTTP requests. In this tutorial, we will use [Postman API client](https://www.postman.com/api-platform/api-client/), so that you can quickly test the API with a user-friendly interface.
{{% /notice %}}

Example request using curl:

```bash
curl --location 'http://yourdomain.test/admin-api/access_token' \
--form 'grant_type="client_credentials"' \
--form 'client_id="your_client_id"' \
--form 'client_secret="your_client_secret"' \
--form 'scope[]="api_client_read"' \
--form 'scope[]="api_client_write"' \
--form 'scope[]="customer_group_read"' \
--form 'scope[]="customer_group_write"'
```

Example request using Postman:

{{< figure src="../img/postman_access_token.png" title="Screenshot from Postman where we retrieve access token for our client" >}}

{{% notice %}}
As you can see in the screenshot, we are using project variables in Postman. This way, you can easily switch between different environments (like development, staging, and production) and don't have to change the request data (URL, client_id, etc.) every time you switch environments. You also won't find Authorization header in the request because it's generated automatically by Postman, thanks to ["Inherit authorization from parent"](https://learning.postman.com/docs/sending-requests/authorization/specifying-authorization-details/#inherit-authorization) option.
{{% /notice %}}

After you send the request, you should receive a response with the access token. If you receive information saying "Use HTTPS response", it means that you need to use HTTPS to connect to the API. As mentioned earlier, you can turn off this check in the PrestaShop back office.

If you still have problems with the connection, make sure that the API is enabled in the PrestaShop back office. You can check this in the Advanced Parameters -> Admin API -> Configuration page.

If everything goes well, the response should look similar to this:

```json
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJ0ZXN0X2NsaWVudCIsImp0aSI6ImQ5OTQzYWQzNDgwNDA3N2QyZGQ5MTBmM2E3YTVmYTdiZTQ1YmMyNzZmM2VmMjA2MGJmZjg2MzhlMzA4ZWE3OWY1ZWI1NzQ0ZmI0N2Y4MDU2IiwiaWF0IjoxNzE2Mjg2MjE5LjQ2NDA3OCwibmJmIjoxNzE2Mjg2MjE5LjQ2NDA4LCJleHAiOjE3MTYyODk4MTkuNDYzMDEsInN1YiI6IiIsInNjb3BlcyI6WyJpc19hdXRoZW50aWNhdGVkIiwiYXBpX2NsaWVudF9yZWFkIiwiYXBpX2NsaWVudF93cml0ZSIsImN1c3RvbWVyX2dyb3VwX3JlYWQiLCJjdXN0b21lcl9ncm91cF93cml0ZSJdfQ.Q6kK0Pl1HVAVrzn5xUrzRO1VSUaw-ygTn9D_rKlfjW3gllUWJiWRaA_pM53RtLId1LkcAfW8nW27CFhQH7TQqLCn4vUPD2t6_s7-3WX_HIqe6MHExib2mW7u_ZXT3bSOyPUOjWIcZsISQR1-noZfOEYDkvgnKDC250zieVqMELgxclMFXKdiLhn83GJnCW35llB1TwAACxdV1uJ_emZGCR3Tsy2IK1pSKPRAb2h-gBre8hqtCmUZ5pdM_L6D_EuUM-aB6iQENiCD6ECmSvqvsqkd3RB73s7PntwniUUafD2GHap1Ttw8pOF7omtT3X0ZLssSX1eQMPDw6JGLFz9caw"
}
```

You can now use the access token to authenticate your client in the next requests. You can use it by providing the `Authorization` header with the value `Bearer YOUR_ACCESS_TOKEN`.

Remember that the access token is valid for a limited time. After it expires, you need to get a new one. It is also crucial to provide scopes that your client can use. If you don't give the correct scopes, you won't be able to access some endpoints.

Access token is a JWT token. JWT stands for JSON Web Token that contains information about the client and the scopes it can use. It's signed with a secret key, so you can be sure that the token is valid and hasn't been tampered with. You can go to [jwt.io](https://jwt.io/) to see the token's content after you decode it.
Loading

0 comments on commit 0b42d68

Please sign in to comment.