#How to get your App token

Implement the required parts of the App activation process and receive an App access token for querying your PIM API.

Use case:
App Workflow
Welcome!
This pre-requisite section will follow you all along the tutorials marked with the use case "App workflow".
The workflow starts with this tutorial that will guide you through the creation of a draft App. At the end of this tutorial, your draft App will receive an access token and will be able to call the REST API.
Let's start!

#What you will learn

In this tutorial, we provide a guide on how to implement the required parts of your App for the activation process based on OAuth 2.0 with Authorization Code. At the end of this tutorial, your App will receive an Access Token and will be able to call the REST API of a PXM Studio.

Examples in this tutorial use languages without any framework or library and, consequently, don't follow all the recommended best practices. We strongly encourage you to adapt those examples with the framework or library of your choice.

If you prefer to start with a functional App (in PHP), have a look here

#Step 1: Expose your activation and callback URLs

First, your application must expose an activation URL.

In our example, we won't do additional steps (like authentification), so we will launch the Authorization Request immediately in this Activation URL.


    // Let's create an `activate.php` file
    
    $oauthClientId = '<CLIENT_ID>';
    $getAuthorizationUrl = '%s/connect/apps/v1/authorize?%s';
    $scopes = ['read_products', 'write_products', 'delete_products'];
    
    session_start();
    
    $pimUrl = $_GET['pim_url'];
    if (empty($pimUrl)) {
        exit('Missing PIM URL in the query');
    }
    
    // create a random state for preventing cross-site request forgery
    $state = bin2hex(random_bytes(10));
    
    // Store in the user session the state and the PIM URL
    $_SESSION['oauth2_state'] = $state;
    $_SESSION['pim_url'] = $pimUrl;
    
    // Build the parameters for the Authorization Request
    // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
    $authorizeUrlParams = http_build_query([
        'response_type' => 'code',
        'client_id' => $oauthClientId,
        'scope' => implode(' ', $scopes),
        'state' => $state,
    ]);
    
    // Build the url for the Authorization Request using the PIM URL
    $url = sprintf($getAuthorizationUrl, $pimUrl, $authorizeUrlParams);
    header('Location: '.$url);
    exit;
    

    import express from 'express';
    import crypto from 'crypto';
    
    const app = express();
    
    app.get('/activate', (req, res, next) => {
      try {
        const clientId = "AKENEO_CLIENT_ID";
        const scopes = [
          "read_products",
          "read_catalog_structure",
          "read_channel_settings",
          "read_channel_localization",
          "read_attribute_options",
          "read_catalogs",
          "write_catalogs",
          "delete_catalogs",
        ];
        const session = req.session;
    
        const pimUrl = req.query.pim_url;
        if (!pimUrl) {
          throw new Error(
            "Can't retrieve PIM url, please restart the authorization process."
          );
        }
    
        // Create a random state for preventing cross-site request forgery
        const state = crypto.randomBytes(64).toString("hex");
    
        // Store in the user session the state and the PIM URL
        session.state = state;
        session.pim_url = pimUrl;
    
        // Build the parameters for the Authorization Request
        // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
        const params = new URLSearchParams({
          response_type: "code",
          client_id: clientId,
          scope: scopes.join(" "),
          state: state,
        });
    
        // Build the url for the Authorization Request using the PIM URL
        const authorizeUrl =
          pimUrl + "/connect/apps/v1/authorize?" + params.toString();
    
        res.redirect(authorizeUrl);
      } catch (err) {
        next(err);
      }
    });
    
    

    import secrets
    from urllib.parse import urljoin
    
    params # data from your request handling
    storage # your own memory system
    
    # Retrieve GET query params from your own framework / http handler
    pim_url: str = params.get('pim_url')
    
    # Retrieve your app's Client ID with your own system
    client_id: str = storage.get("CLIENT_ID")
    
    # The activate URL should have the pim_url param
    if not pim_url:
        # Return a Bad request response via your own framework / http server
        return response(502, {"message": "Bad request"})
    
    # Store the PIM url value with your own system
    storage.set("PIM_URL", pim_url)
    
    # Set the access scopes, take care of the 254 chars max !
    scopes: str = 'read_products write_products'
    
    # Set a new security state secret and store it with your own system
    state: str = secrets.token_hex(32)
    storage.set("APP_STATE", state)
    
    # Redirect to the PIM with "connect" options needed
    redirect_url: str = urljoin(
        pim_url,
        f"/connect/apps/v1/authorize"
        f"?response_type=code"
        f"&client_id={client_id}"
        f"&scope={scopes}"
        f"&state={state}",
    )
    # Set the redirection response with your own framework / http server
    return redirect(redirect_url)
    

    import java.security.SecureRandom;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.apache.tomcat.util.buf.HexUtils;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    public class App {
        static final String OAUTH_CLIENT_ID = "CLIENT_ID";
        static final String OAUTH_SCOPES = "read_products write_products";
        
        @GetMapping("/activate")
        public void activate(
                HttpServletRequest request,
                HttpSession session,
                HttpServletResponse response) throws Exception {
            // Create a random state for preventing cross-site request forgery
            byte[] randomBytes = new byte[10];
            new SecureRandom().nextBytes(randomBytes);
            String state = HexUtils.toHexString(randomBytes);
        
            Object pimUrl = request.getParameter("pim_url");
            if (pimUrl == null) {
                throw new Exception("Missing PIM URL in the query");
            }
        
            // Store in the user session the state and the PIM URL
            session.setAttribute("oauth2_state", state);
            session.setAttribute("pim_url", pimUrl.toString());
        
            // Build url for the Authorization Request
            // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
            String authorizeUrl = pimUrl + "/connect/apps/v1/authorize" + "?response_type=code"
                    + "&client_id=" + OAUTH_CLIENT_ID
                    + "&scope=" + OAUTH_SCOPES + "&state=" + state;
        
            // Redirect the user to the Authorization URL
            response.sendRedirect(authorizeUrl);
        }
    }
    

Then, your application must expose a callback URL.


    // Let's create a `callback.php` file
    
    require_once __DIR__ . '/../vendor/autoload.php';
    
    $oauthClientId = '<CLIENT_ID>';
    $oauthClientSecret = '<CLIENT_SECRET>';
    $generateTokenUrl = '/connect/apps/v1/oauth2/token';
    
    session_start();
    
    // We check if the received state is the same as in the session, for security.
    $sessionState = $_SESSION['oauth2_state'] ?? '';
    $state = $_GET['state'] ?? '';
    if (empty($state) || $state !== $sessionState) {
        exit('Invalid state');
    }
    
    $authorizationCode = $_GET['code'] ?? '';
    if (empty($authorizationCode)) {
        exit('Missing authorization code');
    }
    
    $pimUrl = $_SESSION['pim_url'] ?? '';
    if (empty($pimUrl)) {
        exit('No PIM url in session');
    }
    
    // Generate code for token request
    $codeIdentifier = bin2hex(random_bytes(30));
    $codeChallenge = hash('sha256', $codeIdentifier . $oauthClientSecret);
    
    // Build form data to post
    $accessTokenRequestPayload = [
        'client_id' => $oauthClientId,
        'code_identifier' => $codeIdentifier,
        'code_challenge' => $codeChallenge,
        'code' => $authorizationCode,
        'grant_type' => 'authorization_code',
    ];
    
    // If you haven't set your client yet, please install Guzzle by following the official documentation:
    // https://docs.guzzlephp.org/en/stable/overview.html#installation
    $client = new GuzzleHttp\Client(['base_uri' => $pimUrl]);
    
    // Make an authenticated call to the API
    $accessTokenUrl = $pimUrl . $generateTokenUrl;
    $response = $client->post($accessTokenUrl, ['form_params' => $accessTokenRequestPayload]);
    
    // Convert json response to array
    $contents = json_decode($response->getBody()->getContents(), true);
    
    var_export($contents);
    

    import express from 'express';
    import crypto from 'crypto';
    import fetch from 'node-fetch'; // https://www.npmjs.com/package/node-fetch
    
    const app = express();
    
    app.get('/callback', async (req, res, next) => {
      try {
        const appClientSecret = "CLIENT_SECRET";
        const appClientId = "CLIENT_ID";
    
        const session = req.session;
    
        const pimUrl = session.pim_url;
        const state = req.query.state;
        const authorizationCode = req.query.code;
    
        if (!pimUrl) {
          throw new Error(
            "Can't retrieve PIM url, please restart the authorization process."
          );
        }
    
        // We check if the received state is the same as in the session, for security.
        if (!state || state != session.state) {
          throw new Error("Invalid state");
        }
    
        if (!authorizationCode) {
          throw new Error("Missing authorization code");
        }
    
        // Generate code for token request
        const codeidentifier = crypto.randomBytes(64).toString("hex");
        const codeChallenge = crypto
          .createHash("sha256")
          .update(codeidentifier + appClientSecret)
          .digest("hex");
    
        // Build form data to post
        const accessTokenRequestPayload = new URLSearchParams({
          grant_type: "authorization_code",
          code: authorizationCode,
          client_id: appClientId,
          code_identifier: codeidentifier,
          code_challenge: codeChallenge,
        });
    
        // Make an authenticated call to the API
        const accessTokenUrl = pimUrl + "/connect/apps/v1/oauth2/token";
        const response = await fetch(accessTokenUrl, {
          method: "post",
          body: accessTokenRequestPayload,
          headers: {"Content-Type": "application/x-www-form-urlencoded"},
        });
    
        const result = await response.json();
    
        const accessToken = result.access_token;
    
        console.log(accessToken);
      } catch (err) {
        next(err);
      }
    });
    
    

    import secrets
    import hashlib
    import requests
    from urllib.parse import urljoin
    
    params # data from your request handling
    storage # your own memory system
    
    # Retrieve GET query params from your own framework / http handler
    code: str = params.get('pim_url')
    state: str = params.get('pim_url')
    
    # Retrieve your app's variables with your own system
    pim_url: str = storage.get("PIM_URL")
    app_state: str = storage.get("APP_STATE")
    client_id: str = storage.get("CLIENT_ID")
    client_secret: str = storage.get("CLIENT_SECRET")
    
    # Control the security state integrity previously defined, to avoid attacks
    if state != app_state:
        return response(403, 
            {
                "error": "Forbidden",
                "error_description": "State integrity failed",
            }
        )
    
    # Generate a new challenge code
    # a sha256 concatenation of a code_identifier and the client_secret
    code_identifier: str = secrets.token_hex(32)
    code_challenge: str = hashlib.sha256(f"{code_identifier}{client_secret}".encode("utf-8")).hexdigest()
    
    # Send the payload to the PIM instance, ask for an API Token
    data = requests.post(
        urljoin(
            pim_url,
            "/connect/apps/v1/oauth2/token",
        ),
        data={
            "code": code,
            "grant_type": "authorization_code",
            "client_id": client_id,
            "code_identifier": code_identifier,
            "code_challenge": code_challenge,
        },
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
        },
    ).json()
    
    # Retrieve the fresh token and store it with your own system
    token: str = data.get("access_token")
    storage.set("API_TOKEN", token)
    
    # Set the redirection response with your own framework / http server
    return redirect("/")
    

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpRequest.BodyPublishers;
    import java.net.http.HttpResponse;
    import java.net.http.HttpResponse.BodyHandlers;
    import java.nio.charset.StandardCharsets;
    import java.security.MessageDigest;
    import java.security.SecureRandom;
    
    import javax.json.Json;
    import javax.json.JsonObject;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    
    import org.apache.tomcat.util.buf.HexUtils;
    import org.springframework.web.bind.annotation.GetMapping;
    
    public class App {
        static final String OAUTH_CLIENT_SECRET = "CLIENT_SECRET";
        
        @GetMapping("/callback")
        public String callback(
                HttpServletRequest request,
                HttpSession session) throws Exception {
            Object sessionState = session.getAttribute("oauth2_state");
            String stateParam = request.getParameter("state");
        
            // We check if the received state is the same as in the session, for security.
            if (sessionState == null || !sessionState.equals(stateParam)) {
                throw new Exception("Invalid state");
            }
        
            Object code = request.getParameter("code");
            if (code == null) {
                throw new Exception("Missing authorization code");
            }
        
            Object pimUrl = session.getAttribute("pim_url");
            if (pimUrl == null) {
                throw new Exception("No PIM url in session");
            }
        
            // Generate code challenge
            byte[] randomBytes = new byte[30];
            new SecureRandom().nextBytes(randomBytes);
            String codeIdentifier = HexUtils.toHexString(randomBytes);
        
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] codeChallengeBytes = digest
                    .digest((codeIdentifier + OAUTH_CLIENT_SECRET)
                            .getBytes(StandardCharsets.UTF_8));
            String codeChallenge = HexUtils.toHexString(codeChallengeBytes);
        
            String accessTokenUrl = pimUrl + "/connect/apps/v1/oauth2/token";
        
            JsonObject json = Json.createObjectBuilder()
                    .add("client_id", OAUTH_CLIENT_ID)
                    .add("code_identifier", codeIdentifier)
                    .add("code_challenge", codeChallenge)
                    .add("code", code.toString())
                    .add("grant_type", "authorization_code")
                    .build();
        
            // Do a POST request on the token endpoint
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest authorizeRequest = HttpRequest.newBuilder()
                    .uri(URI.create(accessTokenUrl))
                    .header("Content-Type", "application/json")
                    .POST(BodyPublishers.ofString(json.toString()))
                    .build();
        
            HttpResponse<String> response = client
                    .send(authorizeRequest, BodyHandlers.ofString());
        
            return response.body();
        }
    }
    

You can find more information about the authorization process and code challenge in the following documentation.

#Step 2: Get a public URL for your in development App

if you use a local version of PIM, skip this step

Before proceeding to step 4 create a test App in your developer sandbox, you will need valid URLs to your App. This can be easily resolved with a tunnel to your localhost.

There are several ways to create a tunnel to your localhost such as localhost.run or ngrok. We will use localhost.run for its free and easy setup.

#Initiate localhost tunnel

Initiate localhost tunnel using the following command:


    ssh -R 80:localhost:8080 localhost.run
    

The command above assumes that your local App is available on port 8080 but you can specify any port you want.

#Extract URL from the output

If everything goes well the command will output your public URL for your local app:


    46672a93dd64.lhrtunnel.link tunneled with tls termination, https://46672a93dd64.lhrtunnel.link
    

Your local app is now available at https://46672a93dd64.lhrtunnel.link. You may now use it for your development.

#Step 3: Get your test app credentials

To get credentials for your app, you need to create a test app on your developer sandbox.

First of all, go to Connect, then App Store

#Permissions

If you see Create a test App skip to Connect app, else please enable the developer mode. Create a test app button

To do so, you need to:

  1. Go to System, then Roles
  2. Choose the role you use for your user
  3. In the Permissions tab, scroll down and search for the Developer mode submenu
  4. Select Manage test apps
  5. Don't forget to save your modifications

#Connect app

To create a test App:

  1. On the top right corner, click on Create a test App
  2. Fill in all the required information Test_app_creation_credentials
  3. Then click on Create
  4. Copy/paste credentials in your app configuration file Test_app_creation_credentials
  5. And click on Done
  6. Your test App appears on the App Store page

#Step 4: Connect your test App and access its settings

Test app on the App Store

Connecting a test App is like connecting a published App.

  1. Launch your APP
  2. Click on Connect
  3. Your App opens in a new tab of your browser
  4. Launch the connection process from your App
  5. Follow all the activation process steps, then Confirm
  6. Your test App is now connected with Akeneo PIM! 🔗

Now that your App is connected, you can enjoy all the available App features from the Akeneo PXM Studio UI and test that your App works well.

To access the settings of your connected App on Akeneo PIM, please go to Connected Apps, then click on Manage App. You can also open your App from Akeneo PIM UI, to do so, click on Open app.

Connected test app on Apps

To know more about the step-by-step activation process, please read our article:
How to connect an App?

#Step 5: Use your access token to call the API

At the end of this process, you receive the following response with an access_token:


    {
      "access_token": "Y2YyYjM1ZjMyMmZlZmE5Yzg0OTNiYjRjZTJjNjk0ZTUxYTE0NWI5Zm",
      "token_type": "bearer",
      "scope": "read_products write_products"
    }
    

You can use this token to call the Akeneo PIM REST API.

Next Step
Now that you’ve gotten your App token, continue the journey with Step 2!
2
Develop
your App