Skip to content

Commit eaa8774

Browse files
authored
Merge pull request #6148 from Shopify/migration-to-essentials-cookie
Handles migration of storefront_digest to essentials cookie
2 parents 2087910 + d254f0c commit eaa8774

File tree

4 files changed

+219
-38
lines changed

4 files changed

+219
-38
lines changed

.changeset/six-masks-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme': minor
3+
---
4+
5+
Uses \_shopify_essential cookie for storefront authentication

packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export async function getStorefrontSessionCookiesWithVerification(
9494
storefrontPassword?: string,
9595
): Promise<{[key: string]: string}> {
9696
try {
97-
return await getStorefrontSessionCookies(storeUrl, themeId, storefrontPassword, {
97+
return await getStorefrontSessionCookies(storeUrl, adminSession.storeFqdn, themeId, storefrontPassword, {
9898
'X-Shopify-Shop': adminSession.storeFqdn,
9999
'X-Shopify-Access-Token': adminSession.token,
100100
Authorization: `Bearer ${storefrontToken}`,

packages/theme/src/cli/utilities/theme-environment/storefront-session.test.ts

Lines changed: 165 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {shopifyFetch} from '@shopify/cli-kit/node/http'
99
import {AbortError} from '@shopify/cli-kit/node/error'
1010
import {passwordProtected} from '@shopify/cli-kit/node/themes/api'
1111
import {type AdminSession} from '@shopify/cli-kit/node/session'
12+
import {getThemeKitAccessDomain} from '@shopify/cli-kit/node/context/local'
1213

1314
vi.mock('@shopify/cli-kit/node/http')
1415
vi.mock('@shopify/cli-kit/node/themes/api')
@@ -45,7 +46,11 @@ describe('Storefront API', () => {
4546
)
4647

4748
// When
48-
const cookies = await getStorefrontSessionCookies('https://example-store.myshopify.com', '123456')
49+
const cookies = await getStorefrontSessionCookies(
50+
'https://example-store.myshopify.com',
51+
'example-store.myshopify.com',
52+
'123456',
53+
)
4954

5055
// Then
5156
expect(cookies).toEqual({_shopify_essential: ':AABBCCDDEEFFGGHH==123:'})
@@ -62,13 +67,21 @@ describe('Storefront API', () => {
6267
)
6368
.mockResolvedValueOnce(
6469
response({
65-
status: 200,
66-
headers: {'set-cookie': 'storefront_digest=digest-value; path=/; HttpOnly'},
70+
status: 302,
71+
headers: {
72+
'set-cookie': 'storefront_digest=digest-value; path=/; HttpOnly',
73+
location: 'https://example-store.myshopify.com/',
74+
},
6775
}),
6876
)
6977

7078
// When
71-
const cookies = await getStorefrontSessionCookies('https://example-store.myshopify.com', '123456', 'password')
79+
const cookies = await getStorefrontSessionCookies(
80+
'https://example-store.myshopify.com',
81+
'example-store.myshopify.com',
82+
'123456',
83+
'password',
84+
)
7285

7386
// Then
7487
expect(cookies).toEqual({_shopify_essential: ':AABBCCDDEEFFGGHH==123:', storefront_digest: 'digest-value'})
@@ -135,7 +148,12 @@ describe('Storefront API', () => {
135148
)
136149

137150
// When
138-
const cookies = getStorefrontSessionCookies('https://example-store.myshopify.com', '123456', 'wrongpassword')
151+
const cookies = getStorefrontSessionCookies(
152+
'https://example-store.myshopify.com',
153+
'example-store.myshopify.com',
154+
'123456',
155+
'wrongpassword',
156+
)
139157

140158
// Then
141159
await expect(cookies).rejects.toThrow(
@@ -171,12 +189,153 @@ describe('Storefront API', () => {
171189
)
172190

173191
// When
174-
const cookies = await getStorefrontSessionCookies('https://example-store.myshopify.com', '123456')
192+
const cookies = await getStorefrontSessionCookies(
193+
'https://example-store.myshopify.com',
194+
'example-store.myshopify.com',
195+
'123456',
196+
)
175197

176198
// Then
177199
expect(cookies).toEqual({_shopify_essential: ':AABBCCDDEEFFGGHH==RETRYCOOKIE:'})
178200
expect(shopifyFetch).toHaveBeenCalledTimes(3)
179201
})
202+
203+
test('handles storefront_digest migration to _shopify_essential cookie', async () => {
204+
const originalEssential = ':AABBCCDDEEFFGGHH==123:'
205+
const authenticatedEssential = ':NEWESSENTIAL==456:'
206+
207+
vi.mocked(shopifyFetch)
208+
.mockResolvedValueOnce(
209+
response({
210+
status: 200,
211+
headers: {'set-cookie': `_shopify_essential=${originalEssential}; path=/; HttpOnly`},
212+
}),
213+
)
214+
.mockResolvedValueOnce(
215+
response({
216+
status: 302,
217+
headers: {
218+
'set-cookie': `_shopify_essential=${authenticatedEssential}; path=/; HttpOnly`,
219+
location: 'https://example-store.myshopify.com/',
220+
},
221+
}),
222+
)
223+
224+
// When
225+
const cookies = await getStorefrontSessionCookies(
226+
'https://example-store.myshopify.com',
227+
'example-store.myshopify.com',
228+
'123456',
229+
'password',
230+
)
231+
232+
// Then
233+
expect(cookies).toEqual({
234+
_shopify_essential: authenticatedEssential,
235+
})
236+
})
237+
238+
test('handles theme kit access with _shopify_essential cookies', async () => {
239+
const originalEssential = ':AABBCCDDEEFFGGHH==123:'
240+
const authenticatedEssential = ':NEWESSENTIAL==456:'
241+
242+
vi.mocked(shopifyFetch)
243+
.mockResolvedValueOnce(
244+
response({
245+
status: 200,
246+
headers: {'set-cookie': `_shopify_essential=${originalEssential}; path=/; HttpOnly`},
247+
}),
248+
)
249+
.mockResolvedValueOnce(
250+
response({
251+
status: 302,
252+
headers: {
253+
'set-cookie': `_shopify_essential=${authenticatedEssential}; path=/; HttpOnly`,
254+
location: 'https://example-store.myshopify.com/',
255+
},
256+
}),
257+
)
258+
259+
// When
260+
const cookies = await getStorefrontSessionCookies(
261+
`https://${getThemeKitAccessDomain()}`,
262+
'example-store.myshopify.com',
263+
'123456',
264+
'password',
265+
)
266+
267+
// Then
268+
expect(cookies).toEqual({
269+
_shopify_essential: authenticatedEssential,
270+
})
271+
})
272+
273+
test('handles case when storefront_digest is present (non-migrated case)', async () => {
274+
// Given: storefront_digest is still being used
275+
vi.mocked(shopifyFetch)
276+
.mockResolvedValueOnce(
277+
response({
278+
status: 200,
279+
headers: {'set-cookie': '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly'},
280+
}),
281+
)
282+
.mockResolvedValueOnce(
283+
response({
284+
status: 302,
285+
headers: {
286+
'set-cookie': 'storefront_digest=digest-value; path=/; HttpOnly',
287+
location: 'https://example-store.myshopify.com/',
288+
},
289+
}),
290+
)
291+
292+
// When
293+
const cookies = await getStorefrontSessionCookies(
294+
'https://example-store.myshopify.com',
295+
'example-store.myshopify.com',
296+
'123456',
297+
'password',
298+
)
299+
300+
// Then
301+
expect(cookies).toEqual({
302+
_shopify_essential: ':AABBCCDDEEFFGGHH==123:',
303+
storefront_digest: 'digest-value',
304+
})
305+
})
306+
307+
test('throws error when pasword page does not return a 302', async () => {
308+
// Given: password redirects correctly but _shopify_essential doesn't change (shouldn't happen)
309+
const sameEssential = ':AABBCCDDEEFFGGHH==123:'
310+
311+
vi.mocked(shopifyFetch)
312+
.mockResolvedValueOnce(
313+
response({
314+
status: 200,
315+
headers: {'set-cookie': `_shopify_essential=${sameEssential}; path=/; HttpOnly`},
316+
}),
317+
)
318+
.mockResolvedValueOnce(
319+
response({
320+
status: 200,
321+
}),
322+
)
323+
324+
// When
325+
const cookies = getStorefrontSessionCookies(
326+
'https://example-store.myshopify.com',
327+
'example-store.myshopify.com',
328+
'123456',
329+
'password',
330+
)
331+
332+
// Then
333+
await expect(cookies).rejects.toThrow(
334+
new AbortError(
335+
'Your development session could not be created because the store password is invalid. Please, retry with a different password.',
336+
),
337+
)
338+
})
180339
})
181340

182341
// Tests rely on this function because the 'packages/theme' package cannot

packages/theme/src/cli/utilities/theme-environment/storefront-session.ts

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {parseCookies, serializeCookies} from './cookies.js'
22
import {defaultHeaders} from './storefront-utils.js'
3-
import {shopifyFetch} from '@shopify/cli-kit/node/http'
3+
import {shopifyFetch, Response} from '@shopify/cli-kit/node/http'
44
import {AbortError} from '@shopify/cli-kit/node/error'
55
import {outputDebug} from '@shopify/cli-kit/node/output'
66
import {type AdminSession} from '@shopify/cli-kit/node/session'
@@ -41,31 +41,19 @@ export async function isStorefrontPasswordCorrect(password: string | undefined,
4141
)
4242
}
4343

44-
const locationHeader = response.headers.get('location') ?? ''
45-
let redirectUrl: URL
46-
47-
try {
48-
redirectUrl = new URL(locationHeader, storeUrl)
49-
} catch (error) {
50-
if (error instanceof TypeError) {
51-
return false
52-
}
53-
throw error
54-
}
55-
56-
const storeOrigin = new URL(storeUrl).origin
57-
58-
return response.status === 302 && redirectUrl.origin === storeOrigin
44+
return redirectsToStorefront(response, storeUrl)
5945
}
6046

6147
export async function getStorefrontSessionCookies(
6248
storeUrl: string,
49+
storeFqdn: string,
6350
themeId: string,
6451
password?: string,
6552
headers: {[key: string]: string} = {},
6653
): Promise<{[key: string]: string}> {
6754
const cookieRecord: {[key: string]: string} = {}
6855
const shopifyEssential = await sessionEssentialCookie(storeUrl, themeId, headers)
56+
const storeOrigin = prependHttps(storeFqdn)
6957

7058
cookieRecord._shopify_essential = shopifyEssential
7159

@@ -77,11 +65,15 @@ export async function getStorefrontSessionCookies(
7765
return cookieRecord
7866
}
7967

80-
const storefrontDigest = await enrichSessionWithStorefrontPassword(shopifyEssential, storeUrl, password, headers)
81-
82-
cookieRecord.storefront_digest = storefrontDigest
68+
const additionalCookies = await enrichSessionWithStorefrontPassword(
69+
shopifyEssential,
70+
storeUrl,
71+
storeOrigin,
72+
password,
73+
headers,
74+
)
8375

84-
return cookieRecord
76+
return {...cookieRecord, ...additionalCookies}
8577
}
8678

8779
async function sessionEssentialCookie(
@@ -140,9 +132,10 @@ async function sessionEssentialCookie(
140132
async function enrichSessionWithStorefrontPassword(
141133
shopifyEssential: string,
142134
storeUrl: string,
135+
storeOrigin: string,
143136
password: string,
144137
headers: {[key: string]: string},
145-
) {
138+
): Promise<{[key: string]: string}> {
146139
const params = new URLSearchParams({password})
147140

148141
const response = await shopifyFetch(`${storeUrl}/password`, {
@@ -156,21 +149,45 @@ async function enrichSessionWithStorefrontPassword(
156149
},
157150
})
158151

159-
const setCookies = response.headers.raw()['set-cookie'] ?? []
160-
const storefrontDigest = getCookie(setCookies, 'storefront_digest')
161-
162-
if (!storefrontDigest) {
163-
outputDebug(
164-
`Failed to obtain storefront_digest cookie.\n
165-
-Request ID: ${response.headers.get('x-request-id') ?? 'unknown'}\n
166-
-Body: ${await response.text()}`,
167-
)
152+
if (!redirectsToStorefront(response, storeOrigin)) {
168153
throw new AbortError(
169154
'Your development session could not be created because the store password is invalid. Please, retry with a different password.',
170155
)
171156
}
172157

173-
return storefrontDigest
158+
const setCookies = response.headers.raw()['set-cookie'] ?? []
159+
const storefrontDigest = getCookie(setCookies, 'storefront_digest')
160+
const newShopifyEssential = getCookie(setCookies, '_shopify_essential')
161+
162+
const result: {[key: string]: string} = {}
163+
164+
if (storefrontDigest) {
165+
result.storefront_digest = storefrontDigest
166+
}
167+
168+
if (newShopifyEssential) {
169+
result._shopify_essential = newShopifyEssential
170+
}
171+
172+
return result
173+
}
174+
175+
function redirectsToStorefront(response: Response, storeUrl: string) {
176+
const locationHeader = response.headers.get('location') ?? ''
177+
let redirectUrl: URL
178+
179+
try {
180+
redirectUrl = new URL(locationHeader, storeUrl)
181+
} catch (error) {
182+
if (error instanceof TypeError) {
183+
return false
184+
}
185+
throw error
186+
}
187+
188+
const storeOrigin = new URL(storeUrl).origin
189+
190+
return response.status === 302 && redirectUrl.origin === storeOrigin
174191
}
175192

176193
function getCookie(setCookieArray: string[], cookieName: string) {

0 commit comments

Comments
 (0)