Apps

#Create an App (with code samples)

This feature is available on all SaaS environments and only since v6 for other types of environments.

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 PIM.

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.

#Prerequisites

You must have valid OAuth 2.0 client credentials.

#Activation URL

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
    
    const OAUTH_CLIENT_ID = '<CLIENT_ID>';
    const OAUTH_SCOPES = 'read_products write_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' => OAUTH_CLIENT_ID,
        'scope' => OAUTH_SCOPES,
        'state' => $state,
    ]);
    
    // Build the url for the Authorization Request using the PIM URL
    $authorizeUrl = $pimUrl . '/connect/apps/v1/authorize?' . $authorizeUrlParams;
    
    // Redirect the user to the Authorization URL
    header('Location: ' . $authorizeUrl);
    

    params // data from your request handling
    storage // your own memory system
    
    // Retrieve GET query params from your own framework / http handler
    const { pim_url: pimUrl } = params;
    
    // Retrieve your app's Client ID with your own system
    const clientId = storage.get("CLIENT_ID");
    
    // Set the access scopes, take care of the 254 chars max !
    const scopes = 'read_products write_products'; 
    
    // The activate URL should have the pim_url param
    if (!pimUrl) {
        // 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", pimUrl);
    
    // Set a new security state secret and store the value with your own system
    const state = require('crypto').randomBytes(32).toString("hex");
    storage.set("APP_STATE", state);
    
    // Construct the PIM authorization url, it will be called on "connect" / "open" button
    const redirect_url = `${pimUrl}/connect/apps/v1/authorize` +
        `?response_type=code` +
        `&client_id=${clientId}` +
        `&scope=${scopes}` +
        `&state=${state}`
    
    // Set the redirection response with your own framework / http server
    return redirect(redirect_url);
    
    

    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)
    

#Callback URL

Then, your application must expose a callback URL.


    // Let's create a `callback.php` file:
    
    const OAUTH_CLIENT_ID = '<CLIENT_ID>';
    const OAUTH_CLIENT_SECRET = '<CLIENT_SECRET>';
    
    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');
    }
    
    $codeIdentifier = bin2hex(random_bytes(30));
    $codeChallenge = hash('sha256', $codeIdentifier . OAUTH_CLIENT_SECRET);
    
    $accessTokenUrl = $pimUrl . '/connect/apps/v1/oauth2/token';
    $accessTokenRequestPayload = [
        'client_id' => OAUTH_CLIENT_ID,
        'code_identifier' => $codeIdentifier,
        'code_challenge' => $codeChallenge,
        'code' => $authorizationCode,
        'grant_type' => 'authorization_code',
    ];
    
    // Do a POST request on the token endpoint
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $accessTokenUrl);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $accessTokenRequestPayload);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = json_decode(curl_exec($ch), true);
    
    echo $response['access_token'];
    

    params // data from your request handling
    storage // your own memory system
    
    // Retrieve GET query params from your own framework / http handler
    const { code, state } = params;
    
    // Retrieve your app's Client ID with your own logic
    const pimUrl = storage.get("PIM_URL");
    const appState = storage.get("APP_STATE");
    const clientId = storage.get("CLIENT_ID");
    const clientSecret = storage.get("CLIENT_SECRET");
    
    // Control the security state integrity previously defined, to avoid attacks
    if (state !== appState) {
        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
    const codeIdentifier = require('crypto').randomBytes(32).toString('hex')
    const codeChallenge = require('crypto')
        .createHash('sha256')
        .update(`${codeIdentifier}${clientSecret}`)
        .digest('hex')
    
    // Send the payload to the PIM instance, ask for an API Token
    fetch
        .post(`${storage.get('PIM_URL')}/connect/apps/v1/oauth2/token`, {
            code,
            grant_type: 'authorization_code',
            client_id: clientId,
            code_identifier: codeIdentifier,
            code_challenge: codeChallenge,
        })
        .then(({ data }) => {
            // Retrieve the fresh token and store it with your own system
            const { access_token: accessToken } = data   
            storage.set('API_TOKEN', accessToken)
            redirect('/')
        })
        .catch((data) => {
            // handle error
            res.status(400).send(data)
        })
    
    

    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("/")
    

The Code Challenge is documented here.

And that's it!
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.