Skip to content

Commit 904cc56

Browse files
Remove token exchange step from authentication flow
Made-with: Cursor
1 parent d8414e0 commit 904cc56

5 files changed

Lines changed: 75 additions & 118 deletions

File tree

packages/cli-kit/src/private/node/session.test.ts

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,13 @@ const validIdentityToken: IdentityToken = {
5050
}
5151

5252
const validTokens: OAuthSession = {
53-
admin: {token: 'admin_token', storeFqdn: 'mystore.myshopify.com'},
54-
storefront: 'storefront_token',
55-
partners: 'partners_token',
53+
admin: {token: 'access_token', storeFqdn: 'mystore.myshopify.com'},
54+
storefront: 'access_token',
55+
partners: 'access_token',
5656
userId,
5757
}
5858

59-
const appTokens: Record<string, ApplicationToken> = {
60-
// Admin APIs includes domain in the key
61-
'mystore.myshopify.com-admin': {
62-
accessToken: 'admin_token',
63-
expiresAt: futureDate,
64-
scopes: ['scope', 'scope2'],
65-
},
66-
'storefront-renderer': {
67-
accessToken: 'storefront_token',
68-
expiresAt: futureDate,
69-
scopes: ['scope1'],
70-
},
71-
partners: {
72-
accessToken: 'partners_token',
73-
expiresAt: futureDate,
74-
scopes: ['scope2'],
75-
},
76-
'business-platform': {
77-
accessToken: 'business_platform_token',
78-
expiresAt: futureDate,
79-
scopes: ['scope3'],
80-
},
81-
}
59+
const appTokens: Record<string, ApplicationToken> = {}
8260

8361
const partnersToken: ApplicationToken = {
8462
accessToken: 'custom_partners_token',
@@ -162,7 +140,7 @@ describe('ensureAuthenticated when previous session is invalid', () => {
162140
const got = await ensureAuthenticated(defaultApplications)
163141

164142
// Then
165-
expect(exchangeAccessForApplicationTokens).toBeCalled()
143+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
166144
expect(refreshAccessToken).not.toBeCalled()
167145
expect(businessPlatformRequest).toHaveBeenCalled()
168146
expect(storeSessions).toHaveBeenCalledOnce()
@@ -211,7 +189,7 @@ The CLI is currently unable to prompt for reauthentication.`,
211189
...validIdentityToken,
212190
alias: 'user@example.com',
213191
},
214-
applications: appTokens,
192+
applications: {},
215193
},
216194
},
217195
}
@@ -220,7 +198,7 @@ The CLI is currently unable to prompt for reauthentication.`,
220198
const got = await ensureAuthenticated(defaultApplications)
221199

222200
// Then
223-
expect(exchangeAccessForApplicationTokens).toBeCalled()
201+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
224202
expect(refreshAccessToken).not.toBeCalled()
225203
expect(storeSessions).toBeCalledWith(expectedSessions)
226204
expect(got).toEqual(validTokens)
@@ -239,7 +217,7 @@ The CLI is currently unable to prompt for reauthentication.`,
239217
const got = await ensureAuthenticated(defaultApplications)
240218

241219
// Then
242-
expect(exchangeAccessForApplicationTokens).toBeCalled()
220+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
243221
expect(businessPlatformRequest).toHaveBeenCalled()
244222
expect(storeSessions).toHaveBeenCalledOnce()
245223

@@ -250,26 +228,20 @@ The CLI is currently unable to prompt for reauthentication.`,
250228
expect(got).toEqual(validTokens)
251229
})
252230

253-
test('falls back to userId when no business platform token available', async () => {
231+
test('uses identity token to fetch email during full auth flow', async () => {
254232
// Given
255233
vi.mocked(validateSession).mockResolvedValueOnce('needs_full_auth')
256234
vi.mocked(fetchSessions).mockResolvedValue(undefined)
257-
const appTokensWithoutBusinessPlatform = {
258-
'mystore.myshopify.com-admin': appTokens['mystore.myshopify.com-admin']!,
259-
'storefront-renderer': appTokens['storefront-renderer']!,
260-
partners: appTokens.partners!,
261-
}
262-
vi.mocked(exchangeAccessForApplicationTokens).mockResolvedValueOnce(appTokensWithoutBusinessPlatform)
263235

264236
// When
265237
const got = await ensureAuthenticated(defaultApplications)
266238

267239
// Then
268-
expect(businessPlatformRequest).not.toHaveBeenCalled()
240+
expect(businessPlatformRequest).toHaveBeenCalledWith(expect.any(String), 'access_token')
269241

270-
// Verify the session was stored with userId as alias (fallback)
242+
// Verify the session was stored with email as alias
271243
const storedSession = vi.mocked(storeSessions).mock.calls[0]![0]
272-
expect(storedSession[fqdn]![userId]!.identity.alias).toBe(userId)
244+
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('user@example.com')
273245
})
274246

275247
test('executes complete auth flow if requesting additional scopes', async () => {
@@ -281,7 +253,7 @@ The CLI is currently unable to prompt for reauthentication.`,
281253
const got = await ensureAuthenticated(defaultApplications)
282254

283255
// Then
284-
expect(exchangeAccessForApplicationTokens).toBeCalled()
256+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
285257
expect(refreshAccessToken).not.toBeCalled()
286258
expect(businessPlatformRequest).toHaveBeenCalled()
287259
expect(storeSessions).toHaveBeenCalledOnce()
@@ -320,7 +292,12 @@ describe('when existing session is valid', () => {
320292
vi.mocked(validateSession).mockResolvedValueOnce('ok')
321293
vi.mocked(fetchSessions).mockResolvedValue(validSessions)
322294
vi.mocked(getPartnersToken).mockReturnValue('custom_cli_token')
323-
const expected = {...validTokens, partners: 'custom_partners_token'}
295+
const expected = {
296+
admin: {token: 'access_token', storeFqdn: 'mystore.myshopify.com'},
297+
storefront: 'access_token',
298+
partners: 'custom_partners_token',
299+
userId,
300+
}
324301

325302
// When
326303
const got = await ensureAuthenticated(defaultApplications)
@@ -344,8 +321,16 @@ describe('when existing session is valid', () => {
344321

345322
// Then
346323
expect(refreshAccessToken).toBeCalled()
347-
expect(exchangeAccessForApplicationTokens).toBeCalled()
348-
expect(storeSessions).toBeCalledWith(validSessions)
324+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
325+
const expectedSessions = {
326+
[fqdn]: {
327+
[userId]: {
328+
identity: validIdentityToken,
329+
applications: {},
330+
},
331+
},
332+
}
333+
expect(storeSessions).toBeCalledWith(expectedSessions)
349334
expect(got).toEqual(validTokens)
350335
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
351336
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
@@ -364,8 +349,16 @@ describe('when existing session is expired', () => {
364349

365350
// Then
366351
expect(refreshAccessToken).toBeCalled()
367-
expect(exchangeAccessForApplicationTokens).toBeCalled()
368-
expect(storeSessions).toBeCalledWith(validSessions)
352+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
353+
const expectedSessions = {
354+
[fqdn]: {
355+
[userId]: {
356+
identity: validIdentityToken,
357+
applications: {},
358+
},
359+
},
360+
}
361+
expect(storeSessions).toBeCalledWith(expectedSessions)
369362
expect(got).toEqual(validTokens)
370363
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
371364
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
@@ -385,7 +378,7 @@ describe('when existing session is expired', () => {
385378

386379
// Then
387380
expect(refreshAccessToken).toBeCalled()
388-
expect(exchangeAccessForApplicationTokens).toBeCalled()
381+
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
389382
expect(businessPlatformRequest).toHaveBeenCalled()
390383
expect(storeSessions).toHaveBeenCalledOnce()
391384

@@ -644,7 +637,15 @@ describe('ensureAuthenticated email fetch functionality', () => {
644637
const got = await ensureAuthenticated(defaultApplications)
645638

646639
// Then
647-
expect(storeSessions).toBeCalledWith(validSessions)
640+
const expectedSessions = {
641+
[fqdn]: {
642+
[userId]: {
643+
identity: validIdentityToken,
644+
applications: {},
645+
},
646+
},
647+
}
648+
expect(storeSessions).toBeCalledWith(expectedSessions)
648649
expect(got).toEqual(validTokens)
649650
})
650651

@@ -659,7 +660,15 @@ describe('ensureAuthenticated email fetch functionality', () => {
659660
// Then
660661
// The email fetch is not called during refresh - the session keeps its existing alias
661662
expect(businessPlatformRequest).not.toHaveBeenCalled()
662-
expect(storeSessions).toBeCalledWith(validSessions)
663+
const expectedSessions = {
664+
[fqdn]: {
665+
[userId]: {
666+
identity: validIdentityToken,
667+
applications: {},
668+
},
669+
},
670+
}
671+
expect(storeSessions).toBeCalledWith(expectedSessions)
663672
expect(got).toEqual(validTokens)
664673
})
665674

packages/cli-kit/src/private/node/session.ts

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import {applicationId} from './session/identity.js'
21
import {validateSession} from './session/validate.js'
3-
import {allDefaultScopes, apiScopes} from './session/scopes.js'
2+
import {allDefaultScopes} from './session/scopes.js'
43
import {
5-
exchangeAccessForApplicationTokens,
64
exchangeCustomPartnerToken,
7-
ExchangeScopes,
85
refreshAccessToken,
96
InvalidGrantError,
107
InvalidRequestError,
@@ -293,8 +290,6 @@ The CLI is currently unable to prompt for reauthentication.`,
293290
*/
294291
async function executeCompleteFlow(applications: OAuthApplications): Promise<Session> {
295292
const scopes = getFlattenScopes(applications)
296-
const exchangeScopes = getExchangeScopes(applications)
297-
const store = applications.adminApi?.storeFqdn
298293
if (firstPartyDev()) {
299294
outputDebug(outputContent`Authenticating as Shopify Employee...`)
300295
scopes.push('employee')
@@ -314,20 +309,18 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise<Ses
314309
identityToken = await pollForDeviceAuthorization(deviceAuth.deviceCode, deviceAuth.interval)
315310
}
316311

317-
// Exchange identity token for application tokens
318-
outputDebug(outputContent`CLI token received. Exchanging it for application tokens...`)
319-
const result = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, store)
312+
outputDebug(outputContent`CLI token received (PCAT). Using it directly for API access...`)
320313

321314
// Get the alias for the session (email or userId)
322-
const businessPlatformToken = result[applicationId('business-platform')]?.accessToken
323-
const alias = (await fetchEmail(businessPlatformToken)) ?? identityToken.userId
315+
// Use the PCAT identity token directly to fetch the email
316+
const alias = (await fetchEmail(identityToken.accessToken)) ?? identityToken.userId
324317

325318
const session: Session = {
326319
identity: {
327320
...identityToken,
328321
alias,
329322
},
330-
applications: result,
323+
applications: {},
331324
}
332325

333326
outputCompleted(`Logged in.`)
@@ -340,25 +333,19 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise<Ses
340333
*
341334
* @param session - The session to refresh.
342335
*/
343-
async function refreshTokens(session: Session, applications: OAuthApplications): Promise<Session> {
344-
// Refresh Identity Token
336+
async function refreshTokens(session: Session, _applications: OAuthApplications): Promise<Session> {
337+
// Refresh Identity Token (PCAT) - no exchange needed
345338
const identityToken = await refreshAccessToken(session.identity)
346-
// Exchange new identity token for application tokens
347-
const exchangeScopes = getExchangeScopes(applications)
348-
const applicationTokens = await exchangeAccessForApplicationTokens(
349-
identityToken,
350-
exchangeScopes,
351-
applications.adminApi?.storeFqdn,
352-
)
353339

354340
return {
355341
identity: identityToken,
356-
applications: applicationTokens,
342+
applications: {},
357343
}
358344
}
359345

360346
/**
361347
* Get the application tokens for a given session.
348+
* With PCAT, the identity token can be used directly for all APIs.
362349
*
363350
* @param applications - An object containing the applications we need the tokens for.
364351
* @param session - The current session.
@@ -369,33 +356,26 @@ async function tokensFor(applications: OAuthApplications, session: Session): Pro
369356
userId: session.identity.userId,
370357
}
371358

359+
const pcatToken = session.identity.accessToken
360+
372361
if (applications.adminApi) {
373-
const appId = applicationId('admin')
374-
const realAppId = `${applications.adminApi.storeFqdn}-${appId}`
375-
const token = session.applications[realAppId]?.accessToken
376-
if (token) {
377-
tokens.admin = {token, storeFqdn: applications.adminApi.storeFqdn}
378-
}
362+
tokens.admin = {token: pcatToken, storeFqdn: applications.adminApi.storeFqdn}
379363
}
380364

381365
if (applications.partnersApi) {
382-
const appId = applicationId('partners')
383-
tokens.partners = session.applications[appId]?.accessToken
366+
tokens.partners = pcatToken
384367
}
385368

386369
if (applications.storefrontRendererApi) {
387-
const appId = applicationId('storefront-renderer')
388-
tokens.storefront = session.applications[appId]?.accessToken
370+
tokens.storefront = pcatToken
389371
}
390372

391373
if (applications.businessPlatformApi) {
392-
const appId = applicationId('business-platform')
393-
tokens.businessPlatform = session.applications[appId]?.accessToken
374+
tokens.businessPlatform = pcatToken
394375
}
395376

396377
if (applications.appManagementApi) {
397-
const appId = applicationId('app-management')
398-
tokens.appManagement = session.applications[appId]?.accessToken
378+
tokens.appManagement = pcatToken
399379
}
400380

401381
return tokens
@@ -418,27 +398,6 @@ function getFlattenScopes(apps: OAuthApplications): string[] {
418398
return allDefaultScopes(requestedScopes)
419399
}
420400

421-
/**
422-
* Get the scopes for the given applications.
423-
*
424-
* @param apps - An object containing the applications we need the scopes for.
425-
* @returns An object containing the scopes for each application.
426-
*/
427-
function getExchangeScopes(apps: OAuthApplications): ExchangeScopes {
428-
const adminScope = apps.adminApi?.scopes ?? []
429-
const partnerScope = apps.partnersApi?.scopes ?? []
430-
const storefrontScopes = apps.storefrontRendererApi?.scopes ?? []
431-
const businessPlatformScopes = apps.businessPlatformApi?.scopes ?? []
432-
const appManagementScopes = apps.appManagementApi?.scopes ?? []
433-
return {
434-
admin: apiScopes('admin', adminScope),
435-
partners: apiScopes('partners', partnerScope),
436-
storefront: apiScopes('storefront-renderer', storefrontScopes),
437-
businessPlatform: apiScopes('business-platform', businessPlatformScopes),
438-
appManagement: apiScopes('app-management', appManagementScopes),
439-
}
440-
}
441-
442401
function buildIdentityTokenFromEnv(
443402
scopes: string[],
444403
identityTokenInformation: {accessToken: string; refreshToken: string; userId: string},

packages/cli-kit/src/private/node/session/exchange.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class InvalidGrantError extends ExtendableError {}
1515
export class InvalidRequestError extends ExtendableError {}
1616
class InvalidTargetError extends AbortError {}
1717

18-
export interface ExchangeScopes {
18+
interface ExchangeScopes {
1919
admin: string[]
2020
partners: string[]
2121
storefront: string[]

packages/ui-extensions-dev-console/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"devDependencies": {
2727
"@shopify/react-testing": "^3.0.0",
2828
"@shopify/ui-extensions-test-utils": "3.26.0",
29-
"@types/qrcode.react": "^1.0.2",
3029
"@types/react": "16.14.0",
3130
"@types/react-dom": "^16.9.11",
3231
"@vitejs/plugin-react-refresh": "^1.3.6",

0 commit comments

Comments
 (0)