From 906841d26e914aa2f2c7e890cd86eb42d1298739 Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Fri, 6 Aug 2021 15:20:54 -0400 Subject: [PATCH 01/11] Support Non-Embedded apps again Bring back ShopSession service and Shopify Auth from v16 1 Middleware alias "verify.shopify" that will now points to 2 different Middlewares (VerifyShopifyExternal or VerifyShopifyEmbedded) depending on the "appbridge_enabled" config. --- src/Actions/AuthorizeShop.php | 115 +++++ ...yShopify.php => VerifyShopifyEmbedded.php} | 0 src/Http/Middleware/VerifyShopifyExternal.php | 402 ++++++++++++++++++ src/Services/ShopSession.php | 389 +++++++++++++++++ src/ShopifyAppProvider.php | 10 +- src/Traits/AuthController.php | 41 +- src/resources/config/shopify-app.php | 1 + src/resources/routes/shopify.php | 20 + 8 files changed, 975 insertions(+), 3 deletions(-) create mode 100644 src/Actions/AuthorizeShop.php rename src/Http/Middleware/{VerifyShopify.php => VerifyShopifyEmbedded.php} (100%) create mode 100644 src/Http/Middleware/VerifyShopifyExternal.php create mode 100644 src/Services/ShopSession.php diff --git a/src/Actions/AuthorizeShop.php b/src/Actions/AuthorizeShop.php new file mode 100644 index 00000000..68100578 --- /dev/null +++ b/src/Actions/AuthorizeShop.php @@ -0,0 +1,115 @@ +shopQuery = $shopQuery; + $this->shopCommand = $shopCommand; + $this->shopSession = $shopSession; + } + + /** + * Execution. + * TODO: Rethrow an API exception. + * + * @param ShopDomain $shopDomain The shop ID. + * @param string|null $code The code from Shopify. + * + * @return stdClass + */ + public function __invoke(ShopDomain $shopDomain, ?string $code): stdClass + { + // Get the shop + $shop = $this->shopQuery->getByDomain($shopDomain, [], true); + if ($shop === null) { + // Shop does not exist, make them and re-get + $this->shopCommand->make($shopDomain, NullAccessToken::fromNative(null)); + $shop = $this->shopQuery->getByDomain($shopDomain); + } + + // Return data + $return = [ + 'completed' => false, + 'url' => null, + ]; + + $apiHelper = $shop->apiHelper(); + + // Access/grant mode + $grantMode = $shop->hasOfflineAccess() ? + AuthMode::fromNative(Util::getShopifyConfig('api_grant_mode', $shop)) : + AuthMode::OFFLINE(); + + $return['url'] = $apiHelper->buildAuthUrl($grantMode, Util::getShopifyConfig('api_scopes', $shop)); + + // If there's no code + if (empty($code)) { + return (object) $return; + } + + // if the store has been deleted, restore the store to set the access token + if ($shop->trashed()) { + $shop->restore(); + } + + // We have a good code, get the access details + $this->shopSession->make($shop->getDomain()); + + try { + $this->shopSession->setAccess($apiHelper->getAccessData($code)); + $return['url'] = null; + $return['completed'] = true; + } catch (\Exception $e) { + // Just return the default setting + } + + return (object) $return; + } +} diff --git a/src/Http/Middleware/VerifyShopify.php b/src/Http/Middleware/VerifyShopifyEmbedded.php similarity index 100% rename from src/Http/Middleware/VerifyShopify.php rename to src/Http/Middleware/VerifyShopifyEmbedded.php diff --git a/src/Http/Middleware/VerifyShopifyExternal.php b/src/Http/Middleware/VerifyShopifyExternal.php new file mode 100644 index 00000000..8b2a814b --- /dev/null +++ b/src/Http/Middleware/VerifyShopifyExternal.php @@ -0,0 +1,402 @@ +shopSession = $shopSession; + $this->apiHelper = $apiHelper; + $this->apiHelper->make(); + } + + /** + * Handle an incoming request. + * If HMAC is present, it will try to valiate it. + * If shop is not logged in, redirect to authenticate will happen. + * + * @param Request $request The request object. + * @param \Closure $next The next action. + * + * @throws SignatureVerificationException + * + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + // Grab the domain and check the HMAC (if present) + $domain = $this->getShopDomainFromRequest($request); + $hmac = $this->verifyHmac($request); + + $checks = []; + if ($this->shopSession->guest()) { + if ($hmac === null) { + // Auth flow required if not yet logged in + return $this->handleBadVerification($request, $domain); + } + + // Login the shop and verify their data + $checks[] = 'loginShop'; + } + + // Verify the Shopify session token and verify the shop data + array_push($checks, 'verifyShopifySessionToken', 'verifyShop'); + + // Loop all checks needing to be done, if we get a false, handle it + foreach ($checks as $check) { + $result = call_user_func([$this, $check], $request, $domain); + if ($result === false) { + return $this->handleBadVerification($request, $domain); + } + } + + return $next($request); + } + + /** + * Verify HMAC data, if present. + * + * @param Request $request The request object. + * + * @throws SignatureVerificationException + * + * @return bool|null + */ + private function verifyHmac(Request $request): ?bool + { + $hmac = $this->getHmac($request); + if ($hmac === null) { + // No HMAC, move on... + return null; + } + + // We have HMAC, validate it + $data = $this->getData($request, $hmac[1]); + if ($this->apiHelper->verifyRequest($data)) { + return true; + } + + // Something didn't match + throw new SignatureVerificationException('Unable to verify signature.'); + } + + /** + * Login and verify the shop and it's data. + * + * @param Request $request The request object. + * @param ShopDomainValue $domain The shop domain. + * + * @return bool + */ + private function loginShop(Request $request, ShopDomainValue $domain): bool + { + // Log the shop in + $status = $this->shopSession->make($domain); + if (! $status || ! $this->shopSession->isValid()) { + // Somethings not right... missing token? + return false; + } + + return true; + } + + /** + * Verify the shop is alright, if theres a current session, it will compare. + * + * @param Request $request The request object. + * @param ShopDomainValue $domain The shop domain. + * + * @return bool + */ + private function verifyShop(Request $request, ShopDomainValue $domain): bool + { + // Grab the domain + if (! $domain->isNull() && ! $this->shopSession->isValidCompare($domain)) { + // Somethings not right with the validation + return false; + } + + return true; + } + + /** + * Check the Shopify session token. + * + * @param Request $request The request object. + * @param ShopDomainValue $domain The shop domain. + * + * @return bool + */ + private function verifyShopifySessionToken(Request $request, ShopDomainValue $domain): bool + { + // Ensure Shopify session token is OK + $incomingToken = $request->query('session'); + if ($incomingToken) { + if (! $this->shopSession->isSessionTokenValid($incomingToken)) { + // Tokens do not match + return false; + } + + // Save the session token + $this->shopSession->setSessionToken($incomingToken); + } + + return true; + } + + /** + * Grab the HMAC value, if present, and how it was found. + * Order of precedence is:. + * + * - GET/POST Variable + * - Headers + * - Referer + * + * @param Request $request The request object. + * + * @return null|array + */ + private function getHmac(Request $request): ?array + { + // All possible methods + $options = [ + // GET/POST + DataSource::INPUT()->toNative() => $request->input('hmac'), + // Headers + DataSource::HEADER()->toNative() => $request->header('X-Shop-Signature'), + // Headers: Referer + DataSource::REFERER()->toNative() => function () use ($request): ?string { + $url = parse_url($request->header('referer'), PHP_URL_QUERY); + parse_str($url, $refererQueryParams); + if (! $refererQueryParams || ! isset($refererQueryParams['hmac'])) { + return null; + } + + return $refererQueryParams['hmac']; + }, + ]; + + // Loop through each until we find the HMAC + foreach ($options as $method => $value) { + $result = is_callable($value) ? $value() : $value; + if ($result !== null) { + return [$result, $method]; + } + } + + return null; + } + + /** + * Grab the shop, if present, and how it was found. + * Order of precedence is:. + * + * - GET/POST Variable + * - Headers + * - Referer + * + * @param Request $request The request object. + * + * @return ShopDomainValue + */ + private function getShopDomainFromRequest(Request $request): ShopDomainValue + { + // All possible methods + $options = [ + // GET/POST + DataSource::INPUT()->toNative() => $request->input('shop'), + // Headers + DataSource::HEADER()->toNative() => $request->header('X-Shop-Domain'), + // Headers: Referer + DataSource::REFERER()->toNative() => function () use ($request): ?string { + $url = parse_url($request->header('referer'), PHP_URL_QUERY); + parse_str($url, $refererQueryParams); + if (! $refererQueryParams || ! isset($refererQueryParams['shop'])) { + return null; + } + + return $refererQueryParams['shop']; + }, + ]; + + // Loop through each until we find the HMAC + foreach ($options as $method => $value) { + $result = is_callable($value) ? $value() : $value; + if ($result !== null) { + // Found a shop + return ShopDomain::fromNative($result); + } + } + + // No shop domain found in any source + return NullShopDomain::fromNative(null); + } + + /** + * Grab the data. + * + * @param Request $request The request object. + * @param string $source The source of the data. + * + * @return array + */ + private function getData(Request $request, string $source): array + { + // All possible methods + $options = [ + // GET/POST + DataSource::INPUT()->toNative() => function () use ($request): array { + // Verify + $verify = []; + foreach ($request->query() as $key => $value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + + return $verify; + }, + // Headers + DataSource::HEADER()->toNative() => function () use ($request): array { + // Always present + $shop = $request->header('X-Shop-Domain'); + $signature = $request->header('X-Shop-Signature'); + $timestamp = $request->header('X-Shop-Time'); + + $verify = [ + 'shop' => $shop, + 'hmac' => $signature, + 'timestamp' => $timestamp, + ]; + + // Sometimes present + $code = $request->header('X-Shop-Code') ?? null; + $locale = $request->header('X-Shop-Locale') ?? null; + $state = $request->header('X-Shop-State') ?? null; + $id = $request->header('X-Shop-ID') ?? null; + $ids = $request->header('X-Shop-IDs') ?? null; + + foreach (compact('code', 'locale', 'state', 'id', 'ids') as $key => $value) { + if ($value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + } + + return $verify; + }, + // Headers: Referer + DataSource::REFERER()->toNative() => function () use ($request): array { + $url = parse_url($request->header('referer'), PHP_URL_QUERY); + parse_str($url, $refererQueryParams); + + // Verify + $verify = []; + foreach ($refererQueryParams as $key => $value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + + return $verify; + }, + ]; + + return $options[$source](); + } + + /** + * Handle bad verification by killing the session and redirecting to auth. + * + * @param Request $request The request object. + * @param ShopDomainValue $domain The shop domain. + * + * @throws MissingShopDomainException + * + * @return RedirectResponse + */ + private function handleBadVerification(Request $request, ShopDomainValue $domain) + { + if ($domain->isNull()) { + // We have no idea of knowing who this is, this should not happen + throw new MissingShopDomainException(); + } + + // Set the return-to path so we can redirect after successful authentication + Session::put('return_to', $request->fullUrl()); + + // Kill off anything to do with the session + $this->shopSession->forget(); + + // Mis-match of shops + return Redirect::route( + Util::getShopifyConfig('route_names.authenticate.oauth'), + ['shop' => $domain->toNative()] + ); + } + + /** + * Parse the data source value. + * Handle simple key/values, arrays, and nested arrays. + * + * @param mixed $value + * + * @return string + */ + private function parseDataSourceValue($value): string + { + /** + * Format the value. + * + * @param mixed $val + * + * @return string + */ + $formatValue = function ($val): string { + return is_array($val) ? '["'.implode('", "', $val).'"]' : $val; + }; + + // Nested array + if (is_array($value) && is_array(current($value))) { + return implode(', ', array_map($formatValue, $value)); + } + + // Array or basic value + return $formatValue($value); + } +} diff --git a/src/Services/ShopSession.php b/src/Services/ShopSession.php new file mode 100644 index 00000000..39bb587e --- /dev/null +++ b/src/Services/ShopSession.php @@ -0,0 +1,389 @@ +auth = $auth; + $this->apiHelper = $apiHelper; + $this->cookieHelper = $cookieHelper; + $this->shopCommand = $shopCommand; + $this->shopQuery = $shopQuery; + } + + /** + * Login a shop. + * + * @return bool + */ + public function make(ShopDomainValue $domain): bool + { + // Get the shop + $shop = $this->shopQuery->getByDomain($domain, [], true); + if (! $shop) { + return false; + } + + // Log them in with the guard + $this->cookieHelper->setCookiePolicy(); + $this->auth->guard()->login($shop); + + return true; + } + + /** + * Wrapper for auth->guard()->guest(). + * + * @return bool + */ + public function guest(): bool + { + return $this->auth->guard()->guest(); + } + + /** + * Determines the type of access. + * + * @return string + */ + public function getType(): AuthMode + { + return AuthMode::fromNative(strtoupper(Util::getShopifyConfig('api_grant_mode', $this->getShop()))); + } + + /** + * Determines if the type of access matches. + * + * @param AuthMode $type The type of access to check. + * + * @return bool + */ + public function isType(AuthMode $type): bool + { + return $this->getType()->isSame($type); + } + + /** + * Stores the access token and user (if any). + * Uses database for acess token if it was an offline authentication. + * + * @param ResponseAccess $access + * + * @return self + */ + public function setAccess(ResponseAccess $access): self + { + // Grab the token + $token = AccessToken::fromNative($access['access_token']); + + // Per-User + if (isset($access['associated_user'])) { + // Modify the expire time to a timestamp + $now = Carbon::now(); + $expires = $now->addSeconds($access['expires_in'] - 10); + + // We have a user, so access will live only in session + $this->sessionSet(self::USER, $access['associated_user']); + $this->sessionSet(self::USER_TOKEN, $token->toNative()); + $this->sessionSet(self::USER_EXPIRES, $expires->toDateTimeString()); + } else { + // Update the token in database + $this->shopCommand->setAccessToken($this->getShop()->getId(), $token); + + // Refresh the model + $this->getShop()->refresh(); + } + + return $this; + } + + /** + * Sets the session token from Shopify. + * + * @param string $token The session token from Shopify. + * + * @return self + */ + public function setSessionToken(string $token): self + { + $this->sessionSet(self::SESSION_TOKEN, $token); + + return $this; + } + + /** + * Get the Shopify session token. + * + * @return string|null + */ + public function getSessionToken(): ?string + { + return Session::get(self::SESSION_TOKEN); + } + + /** + * Compare session tokens from Shopify. + * + * @param string|null $incomingToken The session token from Shopify, from the request. + * + * @return bool + */ + public function isSessionTokenValid(?string $incomingToken): bool + { + $currentToken = $this->getSessionToken(); + if ($incomingToken === null || $currentToken === null) { + return true; + } + + return $incomingToken === $currentToken; + } + + /** + * Gets the access token in use. + * + * @param bool $strict Return the token matching the grant type (default: use either). + * + * @return AccessTokenValue + */ + public function getToken(bool $strict = false): AccessTokenValue + { + // Keys as strings + $peruser = AuthMode::PERUSER()->toNative(); + $offline = AuthMode::OFFLINE()->toNative(); + + // Token mapping + $tokens = [ + $peruser => NullableAccessToken::fromNative(Session::get(self::USER_TOKEN)), + $offline => $this->getShop()->getAccessToken(), + ]; + + if ($strict) { + // We need the token matching the type + return $tokens[$this->getType()->toNative()]; + } + + // We need a token either way... + return $tokens[$peruser]->isNull() ? $tokens[$offline] : $tokens[$peruser]; + } + + /** + * Gets the associated user (if any). + * + * @return ResponseAccess|null + */ + public function getUser(): ?ResponseAccess + { + return Session::get(self::USER); + } + + /** + * Determines if there is an associated user. + * + * @return bool + */ + public function hasUser(): bool + { + return $this->getUser() !== null; + } + + /** + * Check if the user has expired. + * + * @return bool + */ + public function isUserExpired(): bool + { + $now = Carbon::now(); + $expires = new Carbon(Session::get(self::USER_EXPIRES)); + + return $now->greaterThanOrEqualTo($expires); + } + + /** + * Forgets anything in session. + * Log out a shop via auth()->guard()->logout(). + * + * @return self + */ + public function forget(): self + { + // Forget session values + $keys = [self::USER, self::USER_TOKEN, self::USER_EXPIRES, self::SESSION_TOKEN]; + foreach ($keys as $key) { + Session::forget($key); + } + + // Logout the shop if logged in + $this->auth->guard()->logout(); + + return $this; + } + + /** + * Checks if the package has everything it needs in session. + * + * @return bool + */ + public function isValid(): bool + { + $currentShop = $this->getShop(); + $currentToken = $this->getToken(true); + $currentDomain = $currentShop->getDomain(); + + $baseValid = ! $currentToken->isEmpty() && ! $currentDomain->isNull(); + if ($this->getUser() !== null) { + // Handle validation of per-user + return $baseValid && ! $this->isUserExpired(); + } + + // Handle validation of standard + return $baseValid; + } + + /** + * Checks if the package has everything it needs in session (compare). + * + * @param ShopDomain $shopDomain The shop to compare validity to. + * + * @return bool + */ + public function isValidCompare(ShopDomain $shopDomain): bool + { + // Ensure domains match + return $this->isValid() && $shopDomain->isSame($this->getShop()->getDomain()); + } + + /** + * Wrapper for auth->guard()->user(). + * + * @return IShopModel|null + */ + public function getShop(): ?IShopModel + { + return $this->auth->guard()->user(); + } + + /** + * Set a session key/value and fix cookie issues. + * + * @param string $key The key. + * @param mixed $value The value. + * + * @return self + */ + protected function sessionSet(string $key, $value): self + { + $this->cookieHelper->setCookiePolicy(); + Session::put($key, $value); + + return $this; + } +} diff --git a/src/ShopifyAppProvider.php b/src/ShopifyAppProvider.php index f78960f2..55a55419 100644 --- a/src/ShopifyAppProvider.php +++ b/src/ShopifyAppProvider.php @@ -30,7 +30,8 @@ use Osiset\ShopifyApp\Http\Middleware\AuthProxy; use Osiset\ShopifyApp\Http\Middleware\AuthWebhook; use Osiset\ShopifyApp\Http\Middleware\Billable; -use Osiset\ShopifyApp\Http\Middleware\VerifyShopify; +use Osiset\ShopifyApp\Http\Middleware\VerifyShopifyEmbedded; +use Osiset\ShopifyApp\Http\Middleware\VerifyShopifyExternal; use Osiset\ShopifyApp\Macros\TokenRedirect; use Osiset\ShopifyApp\Macros\TokenRoute; use Osiset\ShopifyApp\Messaging\Jobs\ScripttagInstaller; @@ -304,7 +305,12 @@ private function bootMiddlewares(): void $this->app['router']->aliasMiddleware('auth.proxy', AuthProxy::class); $this->app['router']->aliasMiddleware('auth.webhook', AuthWebhook::class); $this->app['router']->aliasMiddleware('billable', Billable::class); - $this->app['router']->aliasMiddleware('verify.shopify', VerifyShopify::class); + + if(Util::getShopifyConfig('appbridge_enabled')) { + $this->app['router']->aliasMiddleware('verify.shopify', VerifyShopifyEmbedded::class); + }else{ + $this->app['router']->aliasMiddleware('verify.shopify', VerifyShopifyExternal::class); + } } /** diff --git a/src/Traits/AuthController.php b/src/Traits/AuthController.php index 38868796..31e01726 100644 --- a/src/Traits/AuthController.php +++ b/src/Traits/AuthController.php @@ -6,9 +6,9 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; -use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\View; use Osiset\ShopifyApp\Actions\AuthenticateShop; +use Osiset\ShopifyApp\Actions\AuthorizeShop; use Osiset\ShopifyApp\Exceptions\MissingAuthUrlException; use Osiset\ShopifyApp\Exceptions\SignatureVerificationException; use Osiset\ShopifyApp\Objects\Values\ShopDomain; @@ -52,6 +52,7 @@ public function authenticate(Request $request, AuthenticateShop $authShop) [ 'authUrl' => $result['url'], 'shopDomain' => $shopDomain->toNative(), + 'appUrl' => config('app.url') ] ); } else { @@ -95,4 +96,42 @@ public function token(Request $request) ] ); } + + + /** + * Simply redirects to Shopify's Oauth screen. + * + * @param Request $request The request object. + * @param AuthorizeShop $authShop The action for authenticating a shop. + * + * @return ViewView + */ + public function oauth(Request $request, AuthorizeShop $authShop): ViewView + { + // Setup + $shopDomain = ShopDomain::fromNative($request->get('shop')); + $result = $authShop($shopDomain, null); + + // Redirect + return $this->oauthFailure($result->url, $shopDomain); + } + + /** + * Handles when authentication is unsuccessful or new. + * + * @param string $authUrl The auth URl to redirect the user to get the code. + * @param ShopDomain $shopDomain The shop's domain. + * + * @return ViewView + */ + private function oauthFailure(string $authUrl, ShopDomain $shopDomain): ViewView + { + return View::make( + 'shopify-app::auth.fullpage_redirect', + [ + 'authUrl' => $authUrl, + 'shopDomain' => $shopDomain->toNative(), + ] + ); + } } diff --git a/src/resources/config/shopify-app.php b/src/resources/config/shopify-app.php index 7186666f..59f25235 100644 --- a/src/resources/config/shopify-app.php +++ b/src/resources/config/shopify-app.php @@ -58,6 +58,7 @@ 'route_names' => [ 'home' => env('SHOPIFY_ROUTE_NAME_HOME', 'home'), 'authenticate' => env('SHOPIFY_ROUTE_NAME_AUTHENTICATE', 'authenticate'), + 'authenticate.oauth' => env('SHOPIFY_ROUTE_NAME_AUTHENTICATE_OAUTH', 'authenticate.oauth'), 'authenticate.token' => env('SHOPIFY_ROUTE_NAME_AUTHENTICATE_TOKEN', 'authenticate.token'), 'billing' => env('SHOPIFY_ROUTE_NAME_BILLING', 'billing'), 'billing.process' => env('SHOPIFY_ROUTE_NAME_BILLING_PROCESS', 'billing.process'), diff --git a/src/resources/routes/shopify.php b/src/resources/routes/shopify.php index 28ee6118..8d04dab1 100644 --- a/src/resources/routes/shopify.php +++ b/src/resources/routes/shopify.php @@ -61,6 +61,26 @@ ->name(Util::getShopifyConfig('route_names.authenticate')); } + /* + |-------------------------------------------------------------------------- + | Authenticate: Auth + |-------------------------------------------------------------------------- + | + | This route is hit when a shop comes to the app without a session token + | yet. A token will be grabbed from Shopify AppBridge Javascript + | and then forwarded back to the home route. + | + */ + + if (Util::registerPackageRoute('authenticate.oauth', $manualRoutes)) { + Route::get( + '/authenticate/oauth', + AuthController::class.'@oauth' + ) + ->middleware(['verify.shopify']) + ->name(Util::getShopifyConfig('route_names.authenticate.oauth')); + } + /* |-------------------------------------------------------------------------- | Authenticate: Token From f2f401eefa012da68bbedc6c698f712b9b8eb87d Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Fri, 6 Aug 2021 15:34:52 -0400 Subject: [PATCH 02/11] App url not needed --- src/Traits/AuthController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Traits/AuthController.php b/src/Traits/AuthController.php index 31e01726..0b197e62 100644 --- a/src/Traits/AuthController.php +++ b/src/Traits/AuthController.php @@ -51,8 +51,7 @@ public function authenticate(Request $request, AuthenticateShop $authShop) 'shopify-app::auth.fullpage_redirect', [ 'authUrl' => $result['url'], - 'shopDomain' => $shopDomain->toNative(), - 'appUrl' => config('app.url') + 'shopDomain' => $shopDomain->toNative() ] ); } else { From d6f90260ea787a883ccbc04ffb8b1113b5ca3e80 Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Fri, 6 Aug 2021 15:46:15 -0400 Subject: [PATCH 03/11] Match class name --- src/Http/Middleware/VerifyShopifyEmbedded.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Middleware/VerifyShopifyEmbedded.php b/src/Http/Middleware/VerifyShopifyEmbedded.php index 3f12cf67..2de94931 100644 --- a/src/Http/Middleware/VerifyShopifyEmbedded.php +++ b/src/Http/Middleware/VerifyShopifyEmbedded.php @@ -27,7 +27,7 @@ /** * Responsible for validating the request. */ -class VerifyShopify +class VerifyShopifyEmbedded { /** * The auth manager. From 4a4c7bd3e67f42fa69721894897e18ecb7e5db39 Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Fri, 6 Aug 2021 15:56:17 -0400 Subject: [PATCH 04/11] Missing Util --- src/Http/Middleware/VerifyShopifyExternal.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/Middleware/VerifyShopifyExternal.php b/src/Http/Middleware/VerifyShopifyExternal.php index 8b2a814b..b19b9310 100644 --- a/src/Http/Middleware/VerifyShopifyExternal.php +++ b/src/Http/Middleware/VerifyShopifyExternal.php @@ -15,6 +15,7 @@ use Osiset\ShopifyApp\Objects\Values\NullShopDomain; use Osiset\ShopifyApp\Objects\Values\ShopDomain; use Osiset\ShopifyApp\Services\ShopSession; +use Osiset\ShopifyApp\Util; /** * Response for ensuring an authenticated request. From a69e0a10df13dc42bb8f7c1fc8ad350b976d0aa6 Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Fri, 6 Aug 2021 16:40:44 -0400 Subject: [PATCH 05/11] Create VerifyShopifyEmbedded and VerifyShopifyExternal middlewares that extend the base VerifyShopify middleware --- src/Http/Middleware/VerifyShopify.php | 201 ++++++++++++++++++ src/Http/Middleware/VerifyShopifyEmbedded.php | 181 +--------------- src/Http/Middleware/VerifyShopifyExternal.php | 181 +--------------- 3 files changed, 207 insertions(+), 356 deletions(-) create mode 100644 src/Http/Middleware/VerifyShopify.php diff --git a/src/Http/Middleware/VerifyShopify.php b/src/Http/Middleware/VerifyShopify.php new file mode 100644 index 00000000..05d3eb9c --- /dev/null +++ b/src/Http/Middleware/VerifyShopify.php @@ -0,0 +1,201 @@ +verifyHmac($request); + if ($hmacResult === false) { + // Invalid HMAC + throw new SignatureVerificationException('Unable to verify signature.'); + } + + return $next($request); + } + + /** + * Verify HMAC data, if present. + * + * @param Request $request The request object. + * + * @throws SignatureVerificationException + * + * @return bool|null + */ + protected function verifyHmac(Request $request): ?bool + { + $hmac = $this->getHmacFromRequest($request); + if ($hmac['source'] === null) { + // No HMAC, skip + return null; + } + + // We have HMAC, validate it + $data = $this->getRequestData($request, $hmac['source']); + + return $this->apiHelper->verifyRequest($data); + } + + /** + * Grab the HMAC value, if present, and how it was found. + * Order of precedence is:. + * + * - GET/POST Variable + * - Headers + * - Referer + * + * @param Request $request The request object. + * + * @return array + */ + protected function getHmacFromRequest(Request $request): array + { + // All possible methods + $options = [ + // GET/POST + DataSource::INPUT()->toNative() => $request->input('hmac'), + // Headers + DataSource::HEADER()->toNative() => $request->header('X-Shop-Signature'), + // Headers: Referer + DataSource::REFERER()->toNative() => function () use ($request): ?string { + $url = parse_url($request->header('referer'), PHP_URL_QUERY); + parse_str($url, $refererQueryParams); + if (! $refererQueryParams || ! isset($refererQueryParams['hmac'])) { + return null; + } + + return $refererQueryParams['hmac']; + }, + ]; + + // Loop through each until we find the HMAC + foreach ($options as $method => $value) { + $result = is_callable($value) ? $value() : $value; + if ($result !== null) { + return ['source' => $method, 'value' => $value]; + } + } + + return ['source' => null, 'value' => null]; + } + + /** + * Grab the request data. + * + * @param Request $request The request object. + * @param string $source The source of the data. + * + * @return array + */ + protected function getRequestData(Request $request, string $source): array + { + // All possible methods + $options = [ + // GET/POST + DataSource::INPUT()->toNative() => function () use ($request): array { + // Verify + $verify = []; + foreach ($request->query() as $key => $value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + + return $verify; + }, + // Headers + DataSource::HEADER()->toNative() => function () use ($request): array { + // Always present + $shop = $request->header('X-Shop-Domain'); + $signature = $request->header('X-Shop-Signature'); + $timestamp = $request->header('X-Shop-Time'); + + $verify = [ + 'shop' => $shop, + 'hmac' => $signature, + 'timestamp' => $timestamp, + ]; + + // Sometimes present + $code = $request->header('X-Shop-Code') ?? null; + $locale = $request->header('X-Shop-Locale') ?? null; + $state = $request->header('X-Shop-State') ?? null; + $id = $request->header('X-Shop-ID') ?? null; + $ids = $request->header('X-Shop-IDs') ?? null; + + foreach (compact('code', 'locale', 'state', 'id', 'ids') as $key => $value) { + if ($value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + } + + return $verify; + }, + // Headers: Referer + DataSource::REFERER()->toNative() => function () use ($request): array { + $url = parse_url($request->header('referer'), PHP_URL_QUERY); + parse_str($url, $refererQueryParams); + + // Verify + $verify = []; + foreach ($refererQueryParams as $key => $value) { + $verify[$key] = $this->parseDataSourceValue($value); + } + + return $verify; + }, + ]; + + return $options[$source](); + } + + + /** + * Parse the data source value. + * Handle simple key/values, arrays, and nested arrays. + * + * @param mixed $value + * + * @return string + */ + protected function parseDataSourceValue($value): string + { + /** + * Format the value. + * + * @param mixed $val + * + * @return string + */ + $formatValue = function ($val): string { + return is_array($val) ? '["'.implode('", "', $val).'"]' : $val; + }; + + // Nested array + if (is_array($value) && is_array(current($value))) { + return implode(', ', array_map($formatValue, $value)); + } + + // Array or basic value + return $formatValue($value); + } + +} diff --git a/src/Http/Middleware/VerifyShopifyEmbedded.php b/src/Http/Middleware/VerifyShopifyEmbedded.php index 2de94931..5bc6a344 100644 --- a/src/Http/Middleware/VerifyShopifyEmbedded.php +++ b/src/Http/Middleware/VerifyShopifyEmbedded.php @@ -22,13 +22,15 @@ use Osiset\ShopifyApp\Objects\Values\SessionContext; use Osiset\ShopifyApp\Objects\Values\SessionToken; use Osiset\ShopifyApp\Objects\Values\ShopDomain; +use Osiset\ShopifyApp\Traits\VerifyShopifyMiddleware; use Osiset\ShopifyApp\Util; /** * Responsible for validating the request. */ -class VerifyShopifyEmbedded +class VerifyShopifyEmbedded extends VerifyShopify { + /** * The auth manager. * @@ -36,13 +38,6 @@ class VerifyShopifyEmbedded */ protected $auth; - /** - * The API helper. - * - * @var IApiHelper - */ - protected $apiHelper; - /** * The shop querier. * @@ -89,12 +84,6 @@ public function __construct( */ public function handle(Request $request, Closure $next) { - // Verify the HMAC (if available) - $hmacResult = $this->verifyHmac($request); - if ($hmacResult === false) { - // Invalid HMAC - throw new SignatureVerificationException('Unable to verify signature.'); - } // Continue if current route is an auth or billing route if (Str::contains($request->getRequestUri(), ['/authenticate', '/billing'])) { @@ -199,29 +188,6 @@ protected function handleInvalidShop(Request $request) return $this->installRedirect(ShopDomain::fromRequest($request)); } - /** - * Verify HMAC data, if present. - * - * @param Request $request The request object. - * - * @throws SignatureVerificationException - * - * @return bool|null - */ - protected function verifyHmac(Request $request): ?bool - { - $hmac = $this->getHmacFromRequest($request); - if ($hmac['source'] === null) { - // No HMAC, skip - return null; - } - - // We have HMAC, validate it - $data = $this->getRequestData($request, $hmac['source']); - - return $this->apiHelper->verifyRequest($data); - } - /** * Login and verify the shop and it's data. * @@ -306,49 +272,6 @@ protected function installRedirect(ShopDomainValue $shopDomain): RedirectRespons ); } - /** - * Grab the HMAC value, if present, and how it was found. - * Order of precedence is:. - * - * - GET/POST Variable - * - Headers - * - Referer - * - * @param Request $request The request object. - * - * @return array - */ - protected function getHmacFromRequest(Request $request): array - { - // All possible methods - $options = [ - // GET/POST - DataSource::INPUT()->toNative() => $request->input('hmac'), - // Headers - DataSource::HEADER()->toNative() => $request->header('X-Shop-Signature'), - // Headers: Referer - DataSource::REFERER()->toNative() => function () use ($request): ?string { - $url = parse_url($request->header('referer'), PHP_URL_QUERY); - parse_str($url, $refererQueryParams); - if (! $refererQueryParams || ! isset($refererQueryParams['hmac'])) { - return null; - } - - return $refererQueryParams['hmac']; - }, - ]; - - // Loop through each until we find the HMAC - foreach ($options as $method => $value) { - $result = is_callable($value) ? $value() : $value; - if ($result !== null) { - return ['source' => $method, 'value' => $value]; - } - } - - return ['source' => null, 'value' => null]; - } - /** * Get the token from request (if available). * @@ -374,104 +297,6 @@ protected function getAccessTokenFromRequest(Request $request): ?string return $this->isApiRequest($request) ? $request->bearerToken() : $request->get('token'); } - /** - * Grab the request data. - * - * @param Request $request The request object. - * @param string $source The source of the data. - * - * @return array - */ - protected function getRequestData(Request $request, string $source): array - { - // All possible methods - $options = [ - // GET/POST - DataSource::INPUT()->toNative() => function () use ($request): array { - // Verify - $verify = []; - foreach ($request->query() as $key => $value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - - return $verify; - }, - // Headers - DataSource::HEADER()->toNative() => function () use ($request): array { - // Always present - $shop = $request->header('X-Shop-Domain'); - $signature = $request->header('X-Shop-Signature'); - $timestamp = $request->header('X-Shop-Time'); - - $verify = [ - 'shop' => $shop, - 'hmac' => $signature, - 'timestamp' => $timestamp, - ]; - - // Sometimes present - $code = $request->header('X-Shop-Code') ?? null; - $locale = $request->header('X-Shop-Locale') ?? null; - $state = $request->header('X-Shop-State') ?? null; - $id = $request->header('X-Shop-ID') ?? null; - $ids = $request->header('X-Shop-IDs') ?? null; - - foreach (compact('code', 'locale', 'state', 'id', 'ids') as $key => $value) { - if ($value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - } - - return $verify; - }, - // Headers: Referer - DataSource::REFERER()->toNative() => function () use ($request): array { - $url = parse_url($request->header('referer'), PHP_URL_QUERY); - parse_str($url, $refererQueryParams); - - // Verify - $verify = []; - foreach ($refererQueryParams as $key => $value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - - return $verify; - }, - ]; - - return $options[$source](); - } - - /** - * Parse the data source value. - * Handle simple key/values, arrays, and nested arrays. - * - * @param mixed $value - * - * @return string - */ - protected function parseDataSourceValue($value): string - { - /** - * Format the value. - * - * @param mixed $val - * - * @return string - */ - $formatValue = function ($val): string { - return is_array($val) ? '["'.implode('", "', $val).'"]' : $val; - }; - - // Nested array - if (is_array($value) && is_array(current($value))) { - return implode(', ', array_map($formatValue, $value)); - } - - // Array or basic value - return $formatValue($value); - } - /** * Determine if the request is AJAX or expects JSON. * diff --git a/src/Http/Middleware/VerifyShopifyExternal.php b/src/Http/Middleware/VerifyShopifyExternal.php index b19b9310..ed11729c 100644 --- a/src/Http/Middleware/VerifyShopifyExternal.php +++ b/src/Http/Middleware/VerifyShopifyExternal.php @@ -16,18 +16,14 @@ use Osiset\ShopifyApp\Objects\Values\ShopDomain; use Osiset\ShopifyApp\Services\ShopSession; use Osiset\ShopifyApp\Util; +use Osiset\ShopifyApp\Traits\VerifyShopifyMiddleware; + /** * Response for ensuring an authenticated request. */ -class VerifyShopifyExternal +class VerifyShopifyExternal extends VerifyShopify { - /** - * The API helper. - * - * @var IApiHelper - */ - protected $apiHelper; /** * The shop session helper. @@ -67,14 +63,9 @@ public function handle(Request $request, Closure $next) { // Grab the domain and check the HMAC (if present) $domain = $this->getShopDomainFromRequest($request); - $hmac = $this->verifyHmac($request); $checks = []; if ($this->shopSession->guest()) { - if ($hmac === null) { - // Auth flow required if not yet logged in - return $this->handleBadVerification($request, $domain); - } // Login the shop and verify their data $checks[] = 'loginShop'; @@ -94,33 +85,6 @@ public function handle(Request $request, Closure $next) return $next($request); } - /** - * Verify HMAC data, if present. - * - * @param Request $request The request object. - * - * @throws SignatureVerificationException - * - * @return bool|null - */ - private function verifyHmac(Request $request): ?bool - { - $hmac = $this->getHmac($request); - if ($hmac === null) { - // No HMAC, move on... - return null; - } - - // We have HMAC, validate it - $data = $this->getData($request, $hmac[1]); - if ($this->apiHelper->verifyRequest($data)) { - return true; - } - - // Something didn't match - throw new SignatureVerificationException('Unable to verify signature.'); - } - /** * Login and verify the shop and it's data. * @@ -185,49 +149,6 @@ private function verifyShopifySessionToken(Request $request, ShopDomainValue $do return true; } - /** - * Grab the HMAC value, if present, and how it was found. - * Order of precedence is:. - * - * - GET/POST Variable - * - Headers - * - Referer - * - * @param Request $request The request object. - * - * @return null|array - */ - private function getHmac(Request $request): ?array - { - // All possible methods - $options = [ - // GET/POST - DataSource::INPUT()->toNative() => $request->input('hmac'), - // Headers - DataSource::HEADER()->toNative() => $request->header('X-Shop-Signature'), - // Headers: Referer - DataSource::REFERER()->toNative() => function () use ($request): ?string { - $url = parse_url($request->header('referer'), PHP_URL_QUERY); - parse_str($url, $refererQueryParams); - if (! $refererQueryParams || ! isset($refererQueryParams['hmac'])) { - return null; - } - - return $refererQueryParams['hmac']; - }, - ]; - - // Loop through each until we find the HMAC - foreach ($options as $method => $value) { - $result = is_callable($value) ? $value() : $value; - if ($result !== null) { - return [$result, $method]; - } - } - - return null; - } - /** * Grab the shop, if present, and how it was found. * Order of precedence is:. @@ -273,73 +194,6 @@ private function getShopDomainFromRequest(Request $request): ShopDomainValue return NullShopDomain::fromNative(null); } - /** - * Grab the data. - * - * @param Request $request The request object. - * @param string $source The source of the data. - * - * @return array - */ - private function getData(Request $request, string $source): array - { - // All possible methods - $options = [ - // GET/POST - DataSource::INPUT()->toNative() => function () use ($request): array { - // Verify - $verify = []; - foreach ($request->query() as $key => $value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - - return $verify; - }, - // Headers - DataSource::HEADER()->toNative() => function () use ($request): array { - // Always present - $shop = $request->header('X-Shop-Domain'); - $signature = $request->header('X-Shop-Signature'); - $timestamp = $request->header('X-Shop-Time'); - - $verify = [ - 'shop' => $shop, - 'hmac' => $signature, - 'timestamp' => $timestamp, - ]; - - // Sometimes present - $code = $request->header('X-Shop-Code') ?? null; - $locale = $request->header('X-Shop-Locale') ?? null; - $state = $request->header('X-Shop-State') ?? null; - $id = $request->header('X-Shop-ID') ?? null; - $ids = $request->header('X-Shop-IDs') ?? null; - - foreach (compact('code', 'locale', 'state', 'id', 'ids') as $key => $value) { - if ($value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - } - - return $verify; - }, - // Headers: Referer - DataSource::REFERER()->toNative() => function () use ($request): array { - $url = parse_url($request->header('referer'), PHP_URL_QUERY); - parse_str($url, $refererQueryParams); - - // Verify - $verify = []; - foreach ($refererQueryParams as $key => $value) { - $verify[$key] = $this->parseDataSourceValue($value); - } - - return $verify; - }, - ]; - - return $options[$source](); - } /** * Handle bad verification by killing the session and redirecting to auth. @@ -371,33 +225,4 @@ private function handleBadVerification(Request $request, ShopDomainValue $domain ); } - /** - * Parse the data source value. - * Handle simple key/values, arrays, and nested arrays. - * - * @param mixed $value - * - * @return string - */ - private function parseDataSourceValue($value): string - { - /** - * Format the value. - * - * @param mixed $val - * - * @return string - */ - $formatValue = function ($val): string { - return is_array($val) ? '["'.implode('", "', $val).'"]' : $val; - }; - - // Nested array - if (is_array($value) && is_array(current($value))) { - return implode(', ', array_map($formatValue, $value)); - } - - // Array or basic value - return $formatValue($value); - } } From 61921e69b9a5642fe7fb78a7087a32fdeed89502 Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez Date: Fri, 1 Apr 2022 11:22:03 +0200 Subject: [PATCH 06/11] Fixed oauth redirection loop --- src/Http/Middleware/VerifyShopifyExternal.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Middleware/VerifyShopifyExternal.php b/src/Http/Middleware/VerifyShopifyExternal.php index ed11729c..30f7e78c 100644 --- a/src/Http/Middleware/VerifyShopifyExternal.php +++ b/src/Http/Middleware/VerifyShopifyExternal.php @@ -220,7 +220,7 @@ private function handleBadVerification(Request $request, ShopDomainValue $domain // Mis-match of shops return Redirect::route( - Util::getShopifyConfig('route_names.authenticate.oauth'), + Util::getShopifyConfig('route_names.authenticate'), ['shop' => $domain->toNative()] ); } From 4b4eae279c0cbc32fa0a4febbb3b480d3efb1c20 Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez Date: Fri, 1 Apr 2022 12:48:26 +0200 Subject: [PATCH 07/11] Fixed failing Hmac test, added testing infrastructure --- Dockerfile | 19 +++++++++++++++++++ composer.json | 11 ++++++++--- docker-compose.yml | 11 +++++++++++ src/Http/Middleware/VerifyShopify.php | 6 +++++- tests/Http/Middleware/VerifyShopifyTest.php | 1 - 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d034fb30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM php:7.4-fpm + +RUN apt-get update && apt-get install -y libmcrypt-dev \ + && pecl install mcrypt-1.0.3 \ + && docker-php-ext-enable mcrypt + +RUN apt-get update && apt-get install -y \ + zlib1g-dev \ + libzip-dev \ + unzip +RUN docker-php-ext-install zip +RUN docker-php-ext-install pdo pdo_mysql + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +WORKDIR /var/www +COPY . /var/www + +EXPOSE 3000 diff --git a/composer.json b/composer.json index f00c6d08..d272910b 100644 --- a/composer.json +++ b/composer.json @@ -24,11 +24,12 @@ } ], "require": { - "php": ">=7.2", + "php": ">= 8.0.2", "ext-json": "*", "funeralzone/valueobjects": "^0.5", "jenssegers/agent": "^2.6", "laravel/framework": "^7.0 || ^8.0 || ^9.0", + "laravel/legacy-factories": "^1.3", "osiset/basic-shopify-api": "^9.0 || ^10.0" }, "require-dev": { @@ -36,12 +37,16 @@ "friendsofphp/php-cs-fixer": "^3.0", "mockery/mockery": "^1.0", "orchestra/database": "~3.8 || ~4.0 || ~5.0 || ~6.0", - "orchestra/testbench": "~3.8 || ~4.0 || ~5.0 || ~6.0", + "orchestra/testbench": "*", "phpstan/phpstan": "^0.12", "phpunit/phpunit": "~8.0 || ^9.0" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "platform-check": false }, "extra": { "laravel": { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..f00ca074 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3" +services: + app: + build: + context: . + dockerfile: Dockerfile + working_dir: /var/www/ + container_name: laravel-shopify + volumes: + - ./:/var/www + diff --git a/src/Http/Middleware/VerifyShopify.php b/src/Http/Middleware/VerifyShopify.php index 05d3eb9c..850c7b76 100644 --- a/src/Http/Middleware/VerifyShopify.php +++ b/src/Http/Middleware/VerifyShopify.php @@ -21,6 +21,11 @@ class VerifyShopify */ protected $apiHelper; + public function __construct(IApiHelper $apiHelper){ + $this->apiHelper = $apiHelper; + $this->apiHelper->make(); + } + public function handle(Request $request, Closure $next) { // Verify the HMAC (if available) @@ -52,7 +57,6 @@ protected function verifyHmac(Request $request): ?bool // We have HMAC, validate it $data = $this->getRequestData($request, $hmac['source']); - return $this->apiHelper->verifyRequest($data); } diff --git a/tests/Http/Middleware/VerifyShopifyTest.php b/tests/Http/Middleware/VerifyShopifyTest.php index 047584c7..b009ba5f 100644 --- a/tests/Http/Middleware/VerifyShopifyTest.php +++ b/tests/Http/Middleware/VerifyShopifyTest.php @@ -35,7 +35,6 @@ public function testHmacFail(): void // Server vars [] ); - // Run the middleware $this->runMiddleware(VerifyShopify::class, $newRequest); } From af8fc5c03596218e52a66ec33ce475b825045f82 Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez Date: Fri, 1 Apr 2022 12:54:02 +0200 Subject: [PATCH 08/11] Added config files --- .idea/.gitignore | 8 ++ .idea/laravel-shopify.iml | 148 ++++++++++++++++++++++++ .idea/modules.xml | 8 ++ .idea/php-docker-settings.xml | 24 ++++ .idea/php-test-framework.xml | 14 +++ .idea/php.xml | 209 ++++++++++++++++++++++++++++++++++ .idea/phpunit.xml | 10 ++ .idea/vcs.xml | 6 + 8 files changed, 427 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/laravel-shopify.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/php-docker-settings.xml create mode 100644 .idea/php-test-framework.xml create mode 100644 .idea/php.xml create mode 100644 .idea/phpunit.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..73f69e09 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/laravel-shopify.iml b/.idea/laravel-shopify.iml new file mode 100644 index 00000000..6e3e9c78 --- /dev/null +++ b/.idea/laravel-shopify.iml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..e9800379 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php-docker-settings.xml b/.idea/php-docker-settings.xml new file mode 100644 index 00000000..d771e46a --- /dev/null +++ b/.idea/php-docker-settings.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml new file mode 100644 index 00000000..e856ced7 --- /dev/null +++ b/.idea/php-test-framework.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 00000000..47365f8d --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /usr/local/etc/php/conf.d/docker-php-ext-mcrypt.ini, /usr/local/etc/php/conf.d/docker-php-ext-pdo_mysql.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-zip.ini + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml new file mode 100644 index 00000000..4f8104cf --- /dev/null +++ b/.idea/phpunit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 206369585d73964b71a595e747f15dd2594933a4 Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez <85605917+Pieter-du-Preez@users.noreply.github.com> Date: Fri, 1 Apr 2022 12:57:12 +0200 Subject: [PATCH 09/11] Update ci.yml --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07831cdf..b27eea86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,6 @@ jobs: max-parallel: 3 matrix: php: - - '7.2' - - '7.3' - - '7.4' - '8.0' analysis: [ false ] coverage: [ 'none' ] From f3e735c40d8ef6c140d02d6d7a515f3a64fe13eb Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez <85605917+Pieter-du-Preez@users.noreply.github.com> Date: Fri, 1 Apr 2022 12:58:53 +0200 Subject: [PATCH 10/11] Update ci.yml --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b27eea86..07831cdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,9 @@ jobs: max-parallel: 3 matrix: php: + - '7.2' + - '7.3' + - '7.4' - '8.0' analysis: [ false ] coverage: [ 'none' ] From 2c416e7db39709207ee188c49df90a002925e988 Mon Sep 17 00:00:00 2001 From: Pieter-du-Preez Date: Fri, 1 Apr 2022 16:00:48 +0200 Subject: [PATCH 11/11] Fixed failing tests --- composer.json | 5 ++--- tests/Http/Middleware/VerifyShopifyTest.php | 22 +++++++++++---------- tests/Stubs/Kernel.php | 4 ++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index d272910b..dbb9c85e 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "ext-json": "*", "funeralzone/valueobjects": "^0.5", "jenssegers/agent": "^2.6", - "laravel/framework": "^7.0 || ^8.0 || ^9.0", + "laravel/framework": "^7.0 || ^8.0", "laravel/legacy-factories": "^1.3", "osiset/basic-shopify-api": "^9.0 || ^10.0" }, @@ -45,8 +45,7 @@ "sort-packages": true, "allow-plugins": { "ergebnis/composer-normalize": true - }, - "platform-check": false + } }, "extra": { "laravel": { diff --git a/tests/Http/Middleware/VerifyShopifyTest.php b/tests/Http/Middleware/VerifyShopifyTest.php index b009ba5f..a8c07669 100644 --- a/tests/Http/Middleware/VerifyShopifyTest.php +++ b/tests/Http/Middleware/VerifyShopifyTest.php @@ -6,6 +6,7 @@ use Osiset\ShopifyApp\Exceptions\HttpException; use Osiset\ShopifyApp\Exceptions\SignatureVerificationException; use Osiset\ShopifyApp\Http\Middleware\VerifyShopify; +use Osiset\ShopifyApp\Http\Middleware\VerifyShopifyEmbedded; use Osiset\ShopifyApp\Test\TestCase; class VerifyShopifyTest extends TestCase @@ -59,7 +60,7 @@ public function testSkipAuthenticateAndBillingRoutes(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertTrue($result[0]); } @@ -86,7 +87,8 @@ public function testMissingToken(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); + $this->assertFalse($result[0]); } @@ -118,7 +120,7 @@ public function testMissingTokenAjax(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertFalse($result[0]); } @@ -148,7 +150,7 @@ public function testTokenProcessingAndLoginShop(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertTrue($result[0]); } @@ -175,7 +177,7 @@ public function testTokenProcessingAndNotInstalledShop(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertFalse($result[0]); } @@ -204,7 +206,7 @@ public function testTokenProcessingAndNotInstalledShopAjax(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertFalse($result); } @@ -228,7 +230,7 @@ public function testInvalidToken(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertFalse($result[0]); } @@ -257,7 +259,7 @@ public function testInvalidTokenAjax(): void ); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertFalse($result[0]); } @@ -290,7 +292,7 @@ public function testTokenProcessingAndMissMatchingShops(): void Request::swap($newRequest); // Run the middleware - $result = $this->runMiddleware(VerifyShopify::class, $newRequest); + $result = $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); $this->assertTrue($result[0]); // Run the middleware and change the shop @@ -314,6 +316,6 @@ public function testTokenProcessingAndMissMatchingShops(): void ); $this->expectException(HttpException::class); - $this->runMiddleware(VerifyShopify::class, $newRequest); + $this->runMiddleware(VerifyShopifyEmbedded::class, $newRequest); } } diff --git a/tests/Stubs/Kernel.php b/tests/Stubs/Kernel.php index 15eb36d8..f5b8898b 100644 --- a/tests/Stubs/Kernel.php +++ b/tests/Stubs/Kernel.php @@ -11,7 +11,7 @@ use Osiset\ShopifyApp\Http\Middleware\AuthProxy; use Osiset\ShopifyApp\Http\Middleware\AuthWebhook; use Osiset\ShopifyApp\Http\Middleware\Billable; -use Osiset\ShopifyApp\Http\Middleware\VerifyShopify; +use Osiset\ShopifyApp\Http\Middleware\VerifyShopifyEmbedded; class Kernel extends \Orchestra\Testbench\Http\Kernel { @@ -31,7 +31,7 @@ class Kernel extends \Orchestra\Testbench\Http\Kernel 'throttle' => ThrottleRequests::class, // Added for testing - 'verify.shopify' => VerifyShopify::class, + 'verify.shopify' => VerifyShopifyEmbedded::class, 'auth.webhook' => AuthWebhook::class, 'auth.proxy' => AuthProxy::class, 'billable' => Billable::class,