Skip to content

Commit a2b6df1

Browse files
committed
Add Shopify token exchange flow and remove old OAuth flow
1 parent 0106078 commit a2b6df1

File tree

5 files changed

+137
-44
lines changed

5 files changed

+137
-44
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
"config": {
5454
"sort-packages": true,
5555
"allow-plugins": {
56-
"pestphp/pest-plugin": true
56+
"pestphp/pest-plugin": true,
57+
"phpstan/extension-installer": true
5758
}
5859
},
5960
"extra": {

routes/web.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
<?php
22

3-
use Codelayer\LaravelShopifyIntegration\Http\Controllers\ShopifyAuthController;
43
use Codelayer\LaravelShopifyIntegration\Http\Controllers\ShopifyFallbackController;
54
use Illuminate\Support\Facades\Route;
65

76
Route::middleware(['web'])->group(function () {
87
Route::fallback(ShopifyFallbackController::class)->middleware('shopify.installed');
9-
Route::get('/api/auth', [ShopifyAuthController::class, 'initialize']);
10-
Route::get('/api/auth/callback', [ShopifyAuthController::class, 'callback']);
118
});

src/Http/Controllers/ShopifyAuthController.php

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,21 @@
22

33
namespace Codelayer\LaravelShopifyIntegration\Http\Controllers;
44

5-
use Codelayer\LaravelShopifyIntegration\Events\ShopifyAppInstalled;
6-
use Codelayer\LaravelShopifyIntegration\Lib\AuthRedirection;
7-
use Codelayer\LaravelShopifyIntegration\Lib\CookieHandler;
8-
use Codelayer\LaravelShopifyIntegration\Lib\EnsureBilling;
9-
use Codelayer\LaravelShopifyIntegration\Models\ShopifySession;
10-
use Illuminate\Http\RedirectResponse;
5+
use Codelayer\LaravelShopifyIntegration\Lib\ShopifyOAuth;
116
use Illuminate\Http\Request;
12-
use Shopify\Auth\OAuth;
13-
use Shopify\Utils;
147

158
class ShopifyAuthController extends Controller
169
{
17-
public function initialize(Request $request): RedirectResponse
10+
public function authorizeShopify(Request $request): bool
1811
{
19-
$shop = Utils::sanitizeShopDomain((string) $request->query('shop'));
12+
try {
13+
ShopifyOAuth::authorizeFromRequest($request);
14+
} catch (\Throwable $e) {
15+
report($e);
2016

21-
// Delete any previously created OAuth sessions that were not completed (don't have an access token)
22-
ShopifySession::where('shop', $shop)->where('access_token', null)->delete();
23-
24-
return AuthRedirection::redirect($request);
25-
}
26-
27-
public function callback(Request $request): RedirectResponse
28-
{
29-
$session = OAuth::callback(
30-
cookies: $request->cookie(),
31-
query: $request->query(),
32-
setCookieFunction: [CookieHandler::class, 'saveShopifyCookie'],
33-
);
34-
35-
$host = $request->query('host');
36-
$shop = Utils::sanitizeShopDomain($request->query('shop'));
37-
38-
event(new ShopifyAppInstalled($shop));
39-
40-
$redirectUrl = Utils::getEmbeddedAppUrl($host);
41-
if (config('shopify-integration.billing.required')) {
42-
[$hasPayment, $confirmationUrl] = EnsureBilling::check($session, config('shopify-integration.billing'));
43-
44-
if (! $hasPayment) {
45-
$redirectUrl = $confirmationUrl;
46-
}
17+
return false;
4718
}
4819

49-
return redirect($redirectUrl);
20+
return true;
5021
}
5122
}

src/Http/Middleware/EnsureShopifyInstalled.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
namespace Codelayer\LaravelShopifyIntegration\Http\Middleware;
44

55
use Closure;
6-
use Codelayer\LaravelShopifyIntegration\Lib\AuthRedirection;
6+
use Codelayer\LaravelShopifyIntegration\Lib\ShopifyOAuth;
77
use Codelayer\LaravelShopifyIntegration\Models\ShopifySession;
88
use Illuminate\Http\Request;
9+
use Shopify\Context;
910
use Shopify\Utils;
1011

1112
class EnsureShopifyInstalled
@@ -19,9 +20,15 @@ public function handle(Request $request, Closure $next)
1920
{
2021
$shop = $request->query('shop') ? Utils::sanitizeShopDomain($request->query('shop')) : null;
2122

22-
$appInstalled = $shop && ShopifySession::where('shop', $shop)->where('access_token', '<>', null)->exists();
23+
$appInstalled = $shop && ShopifySession::where('shop', $shop)->where('access_token', '<>', null)->where('scope', Context::$SCOPES->toString())->exists();
2324
$isExitingIframe = preg_match('/^ExitIframe/i', $request->path());
2425

25-
return ($appInstalled || $isExitingIframe) ? $next($request) : AuthRedirection::redirect($request);
26+
if ($appInstalled || $isExitingIframe) {
27+
return $next($request);
28+
}
29+
30+
ShopifyOAuth::authorizeFromRequest($request);
31+
32+
return $next($request);
2633
}
2734
}

src/Lib/ShopifyOAuth.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Codelayer\LaravelShopifyIntegration\Lib;
4+
5+
use Illuminate\Http\Request;
6+
use Psr\Http\Client\ClientExceptionInterface;
7+
use Ramsey\Uuid\Uuid;
8+
use Shopify\Auth\AccessTokenResponse;
9+
use Shopify\Auth\OAuth;
10+
use Shopify\Auth\Session;
11+
use Shopify\Clients\Http;
12+
use Shopify\Context;
13+
use Shopify\Exception\HttpRequestException;
14+
use Shopify\Exception\SessionStorageException;
15+
use Shopify\Exception\UninitializedContextException;
16+
use Shopify\Utils;
17+
18+
class ShopifyOAuth extends OAuth
19+
{
20+
public static function authorizeFromRequest(Request $request): ?Session
21+
{
22+
$encodedSessionToken = self::getSessionTokenHeader($request) ?? self::getSessionTokenFromUrlParam($request);
23+
$decodedSessionToken = Utils::decodeSessionToken($encodedSessionToken);
24+
25+
$dest = $decodedSessionToken['dest'];
26+
$shop = parse_url($dest, PHP_URL_HOST);
27+
28+
$cleanShop = Utils::sanitizeShopDomain($shop);
29+
30+
$session = Utils::loadOfflineSession($cleanShop);
31+
32+
if (empty($session)) {
33+
$session = new Session(
34+
id: OAuth::getOfflineSessionId($cleanShop),
35+
shop: $cleanShop,
36+
isOnline: false,
37+
state: Uuid::uuid4()->toString()
38+
);
39+
}
40+
41+
$accessTokenResponse = ShopifyOAuth::exchangeToken(
42+
shop: $shop,
43+
sessionToken: $encodedSessionToken,
44+
requestedTokenType: 'urn:shopify:params:oauth:token-type:offline-access-token',
45+
);
46+
47+
$session->setAccessToken($accessTokenResponse->getAccessToken());
48+
$session->setScope($accessTokenResponse->getScope());
49+
50+
$sessionStored = Context::$SESSION_STORAGE->storeSession($session);
51+
52+
if (! $sessionStored) {
53+
throw new SessionStorageException(
54+
'OAuth Session could not be saved. Please check your session storage functionality.'
55+
);
56+
}
57+
58+
return $session;
59+
}
60+
61+
/**
62+
* From https://github.com/Shopify/shopify-app-js/blob/ab752293284d344a5e3803271c25e4237e478565/packages/apps/shopify-api/lib/auth/oauth/token-exchange.ts#L27
63+
*
64+
* @throws HttpRequestException
65+
* @throws \JsonException
66+
* @throws ClientExceptionInterface
67+
* @throws UninitializedContextException
68+
*/
69+
public static function exchangeToken(string $shop, string $sessionToken, string $requestedTokenType): AccessTokenResponse
70+
{
71+
Utils::decodeSessionToken($sessionToken);
72+
73+
$body = [
74+
'client_id' => Context::$API_KEY,
75+
'client_secret' => Context::$API_SECRET_KEY,
76+
'grant_type' => 'urn:ietf:params:oauth:grant-type:token-exchange',
77+
'subject_token' => $sessionToken,
78+
'subject_token_type' => 'urn:ietf:params:oauth:token-type:id_token',
79+
'requested_token_type' => $requestedTokenType,
80+
];
81+
82+
$cleanShop = Utils::sanitizeShopDomain($shop);
83+
84+
$client = new Http($cleanShop);
85+
$response = $client->post(path: '/admin/oauth/access_token', body: $body, headers: [
86+
'Content-Type' => 'application/json',
87+
'Accept' => 'application/json',
88+
]);
89+
90+
if ($response->getStatusCode() !== 200) {
91+
throw new HttpRequestException("Failed to get access token: {$response->getDecodedBody()}");
92+
}
93+
94+
$responseBody = $response->getDecodedBody();
95+
96+
return new AccessTokenResponse(
97+
accessToken: $responseBody['access_token'],
98+
scope: $responseBody['scope'],
99+
);
100+
}
101+
102+
private static function getSessionTokenHeader(Request $request): ?string
103+
{
104+
$authorizationHeader = $request->header('authorization');
105+
106+
if (empty($authorizationHeader)) {
107+
return null;
108+
}
109+
110+
return str_replace('Bearer ', '', $authorizationHeader);
111+
}
112+
113+
private static function getSessionTokenFromUrlParam(Request $request): ?string
114+
{
115+
return $request->get('id_token');
116+
}
117+
}

0 commit comments

Comments
 (0)