Search code examples
shopware6shopware6-app

How to authenticate Shopware 6 <base-app-url> correctly


With the Admin SDK it's possible to further enrich the administration in Shopware 6. As in the installation guide for apps stated, an entry point (base-app-url) needs to be provided in the manifest file of an app.

Since every request needs to be authenticated properly, this GET request also needs authentication. However, I am not able to authenticate this one in the same way as I am successfully doing it with the GET request from modules.

The base-app-url request looks the following (in my case with some [custom] entity privileges):

http://localhost:3000/sdk?location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380

The shop has the following data

Shop {
  _id: 'sbzqJiPRrbHAlC2K',
  _url: 'http://localhost:8888',
  _secret: '3c5a2f031006791f2aca40ffa22e8febbc8a53d8',
  _apiKey: 'SWIAB2PVODCWSLZNDMC5ZM1XWA',
  _secretKey: 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
}

I am currently authenticating my modules like the following (Node.js):

const SHOPWARE_SHOP_SIGNATURE = 'shopware-shop-signature';

export function authenticateGetRequest(req: Request, shop: Shop): void {
    // e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380
    const signature = getSignatureFromQuery(req);

    verifySignature(shop.secret, removeParamsFromQuery(req), signature);
}

function getSignatureFromQuery(req: Request): string {
    if (!req.query[SHOPWARE_SHOP_SIGNATURE]) {
        throw new Error('Signature is not present in request!');
    }

    return req.query[SHOPWARE_SHOP_SIGNATURE] as string;
}

function removeParamsFromQuery(req: Request): string {
    // Some code
    // Returns following string - Does neither work for base-app-url nor for module GET requests:
    // 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964'

    // If the string follows this pattern, it works only for modules:
    // shop-id={id}&shop-url={url}&timestamp={ts}&sw-version={v}&sw-context-language={cl}&sw-user-language={ul}
}

function verifySignature(secret: string, message: string, signature: string): void {
    const hmac = crypto.createHmac('sha256', secret).update(message).digest('hex');

    if (hmac !== signature) {
        throw new Error('Signature could not be verified!');
    }
}

However the base-app-url cannot be verified correctly and the "Signature could not be verified!" error is thrown.

What am I doing wrong here?

More info:

Additionally I added a GET request for a module where everything is working:

http://localhost:3000/faq?shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53

The resulting string can be verified:

shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE

Try out following code in some php sandbox environment:

<?php

$message = 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
$secret = '3c5a2f031006791f2aca40ffa22e8febbc8a53d8';
$signature = '0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53';

$hmac = hash_hmac('sha256', $message, $secret);

if (!hash_equals($hmac, $signature)) {
    echo 'Signature not valid';
} else {
    echo 'Signature valid';
}

SOLUTION:

Express decodes the query strings automatically with req.query depending on your express configuration. Keep in mind to validate the hmac with encoded query params as they are passed from shopware.

In my case the only difference where the decoded privileges and they looked like this:

&privileges={"read":["language","ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"create":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"update":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"delete":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"]}

But they need to look like this:

&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D

Solution

  • Looking at the QuerySigner, this is how the signature is generated on the side of Shopware with the actual arguments:

    hash_hmac(
      'sha256', 
      'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE', 
      'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
    );
    // 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a
    

    Given on your side you remove the shopware-shop-signature query param AND that the secrets are equal on both sides, you should be able to regenerate the matching signature.

    const crypto = require('crypto');
    const message = 'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
    const hmac = crypto.createHmac('sha256', 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg').update(message).digest('hex');
    // 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a 
    

    So in theory your code looks fine. Verify that the query string matches exactly. Things to check:

    • Maybe your node server decodes the url entities unwantedly?
    • Does your node serve escape special characters in the query string?
    • Do the secrets match on both sides?

    To consider additionally:

    Consider to just point the base-app-url to a static page outside of the scope of your app server instead. As that page will be loaded inside an iframe, you can use client side javascript to read the query parameters and, only if necessary, make requests to your app server using the credentials from inside the iframe. Keep in mind you really only need the authentication if you need to handle personalized data, otherwise you might as well serve static assets without the need for authentication.