Skip to content

Commit

Permalink
Merge pull request #8 from Shape-and-Shift/feature/add-sw-iframe-midd…
Browse files Browse the repository at this point in the history
…leware

Add Sw Iframe Middleware
  • Loading branch information
ChristopherDosin authored and pbtkhoa committed Nov 9, 2022
2 parents 9a0bff9 + 7ffa63c commit a68fdf3
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### 1.3.0
- Add SwAppIframeMiddleware to verify incoming requests from Iframe Shopware

### 1.2.1
- Fix wrong shopId parameter

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Your app is now ready to install by a Shopware application!
## Usage
- Context, ShopRepository auto-binding
- SwAppMiddleware _(alias: 'sas.app.auth')_: A middleware to verify incoming webhook requests
- SwAppIframeMiddleware _(alias: 'sas.app.auth.iframe')_: A middleware to verify incoming requests from Iframe Shopware

## Change log
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "sas/shopware-laravel-sdk",
"description": "Shopware SDK for Laravel 8",
"type": "library",
"version": "1.2.1",
"version": "1.3.0",
"require": {
"php": "^7.4 || ^8.0",
"ext-json": "*",
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions src/Http/Middleware/SwAppIframeMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types=1);

namespace Sas\ShopwareLaravelSdk\Http\Middleware;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Sas\ShopwareLaravelSdk\Models\SwShop;
use Vin\ShopwareSdk\Data\Webhook\Shop;
use Vin\ShopwareSdk\Data\Webhook\ShopRequest;
use Vin\ShopwareSdk\Exception\AuthorizationFailedException;
use Vin\ShopwareSdk\Service\WebhookAuthenticator;

class SwAppIframeMiddleware extends SwAppMiddleware
{
protected function authenticatePostRequest(Request $request): SwShop
{
$requestContent = json_decode($request->getContent(), true);
$sourceRequest = $requestContent['source'];
$shopId = $sourceRequest[ShopRequest::SHOP_ID_REQUEST_PARAMETER];

$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && $this->checkPostRequest($sourceRequest, $shop->shop_secret);

if (!$authenticated) {
throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid');
}

return $shop;
}

private function checkPostRequest(array $sourceRequests, string $shopSecret): bool
{
$shopwareShopSignature = $sourceRequests['shopware-shop-signature'];

unset($sourceRequests[ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER]);

$results = [];
foreach ($sourceRequests as $key => $sourceRequest) {
if (!in_array($key, self::REQUIRED_KEYS)) {
$sourceRequest = urlencode($sourceRequest);
}

$results[$key] = $sourceRequest;
}

$queryString = htmlspecialchars_decode(urldecode(http_build_query($results)));
$hmac = \hash_hmac('sha256', $queryString, $shopSecret);

return \hash_equals($hmac, $shopwareShopSignature);
}
}
112 changes: 72 additions & 40 deletions src/Http/Middleware/SwAppMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Vin\ShopwareSdk\Data\Webhook\ShopRequest;
use Sas\ShopwareLaravelSdk\Models\SwShop;
use Sas\ShopwareLaravelSdk\Repositories\ShopRepository;
use Vin\ShopwareSdk\Data\Webhook\Shop;
use Vin\ShopwareSdk\Data\Webhook\ShopRequest;
use Vin\ShopwareSdk\Exception\AuthorizationFailedException;
use Vin\ShopwareSdk\Service\WebhookAuthenticator;

class SwAppMiddleware
{
private ShopRepository $shopRepository;
public const REQUIRED_KEYS = [
ShopRequest::SHOP_ID_REQUEST_PARAMETER,
ShopRequest::SHOP_URL_REQUEST_PARAMETER,
ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER,
ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER,
ShopRequest::TIME_STAMP_REQUEST_PARAMETER,
];

protected ShopRepository $shopRepository;

public function __construct(ShopRepository $shopRepository)
{
Expand All @@ -22,37 +32,21 @@ public function __construct(ShopRepository $shopRepository)
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null ...$guards
* @param Request $request
* @param Closure $next
* @param string|null ...$guards
* @return mixed
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$authenticated = false;
$shop = null;

if ($request->getMethod() === 'POST' && $this->supportsPostRequest($request)) {
$requestContent = json_decode($request->getContent(), true);
$shopId = $requestContent['source'][ShopRequest::SHOP_ID_REQUEST_PARAMETER];

$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticatePostRequest($shop->shop_secret);
$shop = $this->authenticatePostRequest($request);
} elseif ($request->getMethod() === 'GET' && $this->supportsGetRequest($request)) {
$shopId = $request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER);
$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret);
}elseif ($request->getMethod() === 'DELETE' && $this->supportsGetRequest($request)) {
$shopId = $request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER);
$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret);
}

if (!$authenticated) {
throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid');
$shop = $this->authenticateGetRequest($request);
} elseif ($request->getMethod() === 'DELETE' && $this->supportsGetRequest($request)) {
$shop = $this->authenticateDeleteRequest($request);
}

// TODO: set custom guard for app
Expand All @@ -63,7 +57,18 @@ public function handle(Request $request, Closure $next, ...$guards)
return $next($request);
}

private function supportsPostRequest(Request $request): bool
protected function checkRequiredKeys(array $data): bool
{
foreach (self::REQUIRED_KEYS as $key) {
if (!array_key_exists($key, $data)) {
return false;
}
}

return true;
}

protected function supportsPostRequest(Request $request): bool
{
$requestContent = json_decode($request->getContent(), true);

Expand All @@ -76,26 +81,53 @@ private function supportsPostRequest(Request $request): bool
return $this->checkRequiredKeys($requestContent['source']);
}

private function supportsGetRequest(Request $request): bool
protected function supportsGetRequest(Request $request): bool
{
return $this->checkRequiredKeys($request->query->all());
}

private function checkRequiredKeys(array $data): bool {
$requiredKeys = [
ShopRequest::SHOP_ID_REQUEST_PARAMETER,
ShopRequest::SHOP_URL_REQUEST_PARAMETER,
ShopRequest::SHOPWARE_VERSION_REQUEST_PARAMETER,
ShopRequest::SHOP_SIGNATURE_REQUEST_PARAMETER,
ShopRequest::TIME_STAMP_REQUEST_PARAMETER,
];
protected function authenticatePostRequest(Request $request): SwShop
{
$requestContent = json_decode($request->getContent(), true);
$sourceRequest = $requestContent['source'];
$shopId = $sourceRequest[ShopRequest::SHOP_ID_REQUEST_PARAMETER];

foreach ($requiredKeys as $key) {
if (!array_key_exists($key, $data)) {
return false;
}
$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticatePostRequest($shop->shop_secret);

if (!$authenticated) {
throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid');
}

return true;
return $shop;
}

protected function authenticateGetRequest(Request $request): SwShop
{
$shopId = $request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER);
$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret);

if (!$authenticated) {
throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid');
}

return $shop;
}

protected function authenticateDeleteRequest(Request $request): SwShop
{
$shopId = $request->query->get(ShopRequest::SHOP_ID_REQUEST_PARAMETER);
$shop = $this->shopRepository->getShopById($shopId);

$authenticated = $shop && WebhookAuthenticator::authenticateGetRequest($shop->shop_secret);

if (!$authenticated) {
throw new AuthorizationFailedException($request->getMethod() . ' is not supported or the data is invalid');
}

return $shop;
}
}
7 changes: 7 additions & 0 deletions src/Models/SwShop.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Authenticatable;

/**
* @property string $shop_id
* @property string $shop_url
* @property string $shop_secret
* @property string $api_key
* @property string $secret_key
*/
class SwShop extends Model implements AuthenticatableContract
{
use Authenticatable;
Expand Down
2 changes: 2 additions & 0 deletions src/ServiceProvider/ShopwareSdkServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Sas\ShopwareLaravelSdk\ServiceProvider;

use Sas\ShopwareLaravelSdk\Http\Middleware\SwAppIframeMiddleware;
use Sas\ShopwareLaravelSdk\Http\Middleware\SwAppMiddleware;
use Illuminate\Support\ServiceProvider;
use Sas\ShopwareLaravelSdk\Utils\AppHelper;
Expand Down Expand Up @@ -40,5 +41,6 @@ public function boot(): void
$this->loadRoutesFrom(__DIR__ . '/../routes/app.php');

$this->app->get('router')->aliasMiddleware('sas.app.auth', SwAppMiddleware::class);
$this->app->get('router')->aliasMiddleware('sas.app.auth.iframe', SwAppIframeMiddleware::class);
}
}

0 comments on commit a68fdf3

Please sign in to comment.