Skip to content

Commit 3bae1d2

Browse files
wire up identity client to current identity callsites
1 parent 78b33dc commit 3bae1d2

File tree

7 files changed

+45
-48
lines changed

7 files changed

+45
-48
lines changed

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

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {validateSession} from './session/validate.js'
2020
import {applicationId} from './session/identity.js'
2121
import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js'
2222
import {getCurrentSessionId} from './conf-store.js'
23+
import {getIdentityClient} from './clients/identity/instance.js'
24+
import {IdentityMockClient} from './clients/identity/identity-mock-client.js'
2325
import * as fqdnModule from '../../public/node/context/fqdn.js'
2426
import {themeToken} from '../../public/node/context/local.js'
2527
import {partnersRequest} from '../../public/node/api/partners.js'
@@ -31,7 +33,7 @@ import {vi, describe, expect, test, beforeEach} from 'vitest'
3133

3234
const futureDate = new Date(2022, 1, 1, 11)
3335

34-
const userId = '1234-5678'
36+
const userId = '08978734-325e-44ce-bc65-34823a8d5180'
3537

3638
const defaultApplications: OAuthApplications = {
3739
adminApi: {storeFqdn: 'mystore', scopes: []},
@@ -105,6 +107,8 @@ const invalidSessions: Sessions = {
105107
},
106108
}
107109

110+
const mockIdentityClient = new IdentityMockClient()
111+
108112
vi.mock('../../public/node/context/local.js')
109113
vi.mock('./session/identity')
110114
vi.mock('./session/authorize')
@@ -119,6 +123,7 @@ vi.mock('../../public/node/environment.js')
119123
vi.mock('./session/device-authorization')
120124
vi.mock('./conf-store')
121125
vi.mock('../../public/node/system.js')
126+
vi.mock('./clients/identity/instance.js')
122127

123128
beforeEach(() => {
124129
vi.spyOn(fqdnModule, 'identityFqdn').mockResolvedValue(fqdn)
@@ -149,6 +154,11 @@ beforeEach(() => {
149154
email: 'user@example.com',
150155
},
151156
})
157+
158+
vi.mocked(getIdentityClient).mockImplementation(() => mockIdentityClient)
159+
vi.spyOn(mockIdentityClient, 'refreshAccessToken').mockResolvedValue(validIdentityToken)
160+
vi.spyOn(mockIdentityClient, 'requestAccessToken').mockResolvedValue(validIdentityToken)
161+
vi.spyOn(mockIdentityClient, 'exchangeAccessForApplicationTokens').mockResolvedValue(appTokens)
152162
})
153163

154164
describe('ensureAuthenticated when previous session is invalid', () => {
@@ -172,7 +182,7 @@ describe('ensureAuthenticated when previous session is invalid', () => {
172182
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('user@example.com')
173183

174184
// The userID is cached in memory and the secureStore is not accessed again
175-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
185+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
176186
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
177187
expect(fetchSessions).toHaveBeenCalledOnce()
178188
})
@@ -223,7 +233,7 @@ The CLI is currently unable to prompt for reauthentication.`,
223233
expect(refreshAccessToken).not.toBeCalled()
224234
expect(storeSessions).toBeCalledWith(expectedSessions)
225235
expect(got).toEqual(validTokens)
226-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
236+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
227237
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
228238
expect(fetchSessions).toHaveBeenCalledOnce()
229239
})
@@ -290,7 +300,7 @@ The CLI is currently unable to prompt for reauthentication.`,
290300
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('user@example.com')
291301

292302
expect(got).toEqual(validTokens)
293-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
303+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
294304
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
295305
expect(fetchSessions).toHaveBeenCalledOnce()
296306
})
@@ -309,7 +319,7 @@ describe('when existing session is valid', () => {
309319
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
310320
expect(refreshAccessToken).not.toBeCalled()
311321
expect(got).toEqual(validTokens)
312-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
322+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
313323
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
314324
expect(fetchSessions).toHaveBeenCalledOnce()
315325
})
@@ -328,7 +338,7 @@ describe('when existing session is valid', () => {
328338
expect(exchangeAccessForApplicationTokens).not.toBeCalled()
329339
expect(refreshAccessToken).not.toBeCalled()
330340
expect(got).toEqual(expected)
331-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
341+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
332342
await expect(getLastSeenAuthMethod()).resolves.toEqual('partners_token')
333343
expect(fetchSessions).toHaveBeenCalledOnce()
334344
})
@@ -342,11 +352,11 @@ describe('when existing session is valid', () => {
342352
const got = await ensureAuthenticated(defaultApplications, process.env, {forceRefresh: true})
343353

344354
// Then
345-
expect(refreshAccessToken).toBeCalled()
355+
expect(mockIdentityClient.refreshAccessToken).toBeCalled()
346356
expect(exchangeAccessForApplicationTokens).toBeCalled()
347357
expect(storeSessions).toBeCalledWith(validSessions)
348358
expect(got).toEqual(validTokens)
349-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
359+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
350360
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
351361
expect(fetchSessions).toHaveBeenCalledOnce()
352362
})
@@ -362,11 +372,11 @@ describe('when existing session is expired', () => {
362372
const got = await ensureAuthenticated(defaultApplications)
363373

364374
// Then
365-
expect(refreshAccessToken).toBeCalled()
375+
expect(mockIdentityClient.refreshAccessToken).toBeCalled()
366376
expect(exchangeAccessForApplicationTokens).toBeCalled()
367377
expect(storeSessions).toBeCalledWith(validSessions)
368378
expect(got).toEqual(validTokens)
369-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
379+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
370380
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
371381
expect(fetchSessions).toHaveBeenCalledOnce()
372382
})
@@ -377,13 +387,13 @@ describe('when existing session is expired', () => {
377387

378388
vi.mocked(validateSession).mockResolvedValueOnce('needs_refresh')
379389
vi.mocked(fetchSessions).mockResolvedValue(validSessions)
380-
vi.mocked(refreshAccessToken).mockRejectedValueOnce(tokenResponseError)
390+
vi.spyOn(mockIdentityClient, 'refreshAccessToken').mockRejectedValueOnce(tokenResponseError)
381391

382392
// When
383393
const got = await ensureAuthenticated(defaultApplications)
384394

385395
// Then
386-
expect(refreshAccessToken).toBeCalled()
396+
expect(mockIdentityClient.refreshAccessToken).toBeCalled()
387397
expect(exchangeAccessForApplicationTokens).toBeCalled()
388398
expect(businessPlatformRequest).toHaveBeenCalled()
389399
expect(storeSessions).toHaveBeenCalledOnce()
@@ -393,7 +403,7 @@ describe('when existing session is expired', () => {
393403
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('user@example.com')
394404

395405
expect(got).toEqual(validTokens)
396-
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('1234-5678')
406+
await expect(getLastSeenUserIdAfterAuth()).resolves.toBe('08978734-325e-44ce-bc65-34823a8d5180')
397407
await expect(getLastSeenAuthMethod()).resolves.toEqual('device_auth')
398408
expect(fetchSessions).toHaveBeenCalledOnce()
399409
})
@@ -667,10 +677,11 @@ describe('ensureAuthenticated email fetch functionality', () => {
667677
const tokenResponseError = new InvalidGrantError()
668678
vi.mocked(validateSession).mockResolvedValueOnce('needs_refresh')
669679
vi.mocked(fetchSessions).mockResolvedValue(validSessions)
670-
vi.mocked(refreshAccessToken).mockRejectedValueOnce(tokenResponseError)
680+
vi.spyOn(mockIdentityClient, 'refreshAccessToken').mockRejectedValueOnce(tokenResponseError)
681+
vi.spyOn(mockIdentityClient, 'requestAccessToken').mockResolvedValueOnce(validIdentityToken)
671682
vi.mocked(businessPlatformRequest).mockResolvedValueOnce({
672683
currentUserAccount: {
673-
email: 'fallback@example.com',
684+
email: 'dev@shopify.com',
674685
},
675686
})
676687

@@ -679,7 +690,7 @@ describe('ensureAuthenticated email fetch functionality', () => {
679690

680691
// Then
681692
const storedSession = vi.mocked(storeSessions).mock.calls[0]![0]
682-
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('fallback@example.com')
693+
expect(storedSession[fqdn]![userId]!.identity.alias).toBe('dev@shopify.com')
683694
expect(got).toEqual(validTokens)
684695
})
685696

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import {
55
exchangeAccessForApplicationTokens,
66
exchangeCustomPartnerToken,
77
ExchangeScopes,
8-
refreshAccessToken,
98
InvalidGrantError,
109
InvalidRequestError,
1110
} from './session/exchange.js'
1211
import {IdentityToken, Session, Sessions} from './session/schema.js'
1312
import * as sessionStore from './session/store.js'
14-
import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js'
1513
import {isThemeAccessSession} from './api/rest.js'
1614
import {getCurrentSessionId, setCurrentSessionId} from './conf-store.js'
1715
import {UserEmailQueryString, UserEmailQuery} from './api/graphql/business-platform-destinations/user-email.js'
16+
import {getIdentityClient} from './clients/identity/instance.js'
1817
import {outputContent, outputToken, outputDebug, outputCompleted} from '../../public/node/output.js'
1918
import {firstPartyDev, themeToken} from '../../public/node/context/local.js'
2019
import {AbortError} from '../../public/node/error.js'
@@ -306,13 +305,7 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise<Ses
306305
if (identityTokenInformation) {
307306
identityToken = buildIdentityTokenFromEnv(scopes, identityTokenInformation)
308307
} else {
309-
// Request a device code to authorize without a browser redirect.
310-
outputDebug(outputContent`Requesting device authorization code...`)
311-
const deviceAuth = await requestDeviceAuthorization(scopes)
312-
313-
// Poll for the identity token
314-
outputDebug(outputContent`Starting polling for the identity token...`)
315-
identityToken = await pollForDeviceAuthorization(deviceAuth.deviceCode, deviceAuth.interval)
308+
identityToken = await getIdentityClient().requestAccessToken(scopes)
316309
}
317310

318311
// Exchange identity token for application tokens
@@ -343,7 +336,7 @@ async function executeCompleteFlow(applications: OAuthApplications): Promise<Ses
343336
*/
344337
async function refreshTokens(session: Session, applications: OAuthApplications): Promise<Session> {
345338
// Refresh Identity Token
346-
const identityToken = await refreshAccessToken(session.identity)
339+
const identityToken = await getIdentityClient().refreshAccessToken(session.identity)
347340
// Exchange new identity token for application tokens
348341
const exchangeScopes = getExchangeScopes(applications)
349342
const applicationTokens = await exchangeAccessForApplicationTokens(

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
pollForDeviceAuthorization,
44
requestDeviceAuthorization,
55
} from './device-authorization.js'
6-
import {clientId} from './identity.js'
76
import {IdentityToken} from './schema.js'
87
import {exchangeDeviceCodeForAccessToken} from './exchange.js'
98
import {identityFqdn} from '../../../public/node/context/fqdn.js'
@@ -50,7 +49,6 @@ describe('requestDeviceAuthorization', () => {
5049
const response = new Response(JSON.stringify(data))
5150
vi.mocked(shopifyFetch).mockResolvedValue(response)
5251
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
53-
vi.mocked(clientId).mockReturnValue('clientId')
5452

5553
// When
5654
const got = await requestDeviceAuthorization(['scope1', 'scope2'])
@@ -59,7 +57,7 @@ describe('requestDeviceAuthorization', () => {
5957
expect(shopifyFetch).toBeCalledWith('https://fqdn.com/oauth/device_authorization', {
6058
method: 'POST',
6159
headers: {'Content-type': 'application/x-www-form-urlencoded'},
62-
body: 'client_id=clientId&scope=scope1 scope2',
60+
body: 'client_id=fbdb2649-e327-4907-8f67-908d24cfd7e3&scope=scope1 scope2',
6361
})
6462
expect(got).toEqual(dataExpected)
6563
})
@@ -71,7 +69,6 @@ describe('requestDeviceAuthorization', () => {
7169
Object.defineProperty(response, 'statusText', {value: 'OK'})
7270
vi.mocked(shopifyFetch).mockResolvedValue(response)
7371
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
74-
vi.mocked(clientId).mockReturnValue('clientId')
7572

7673
// When/Then
7774
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError(
@@ -86,7 +83,6 @@ describe('requestDeviceAuthorization', () => {
8683
Object.defineProperty(response, 'statusText', {value: 'OK'})
8784
vi.mocked(shopifyFetch).mockResolvedValue(response)
8885
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
89-
vi.mocked(clientId).mockReturnValue('clientId')
9086

9187
// When/Then
9288
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError(
@@ -102,7 +98,6 @@ describe('requestDeviceAuthorization', () => {
10298
Object.defineProperty(response, 'statusText', {value: 'Not Found'})
10399
vi.mocked(shopifyFetch).mockResolvedValue(response)
104100
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
105-
vi.mocked(clientId).mockReturnValue('clientId')
106101

107102
// When/Then
108103
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError(
@@ -117,7 +112,6 @@ describe('requestDeviceAuthorization', () => {
117112
Object.defineProperty(response, 'statusText', {value: 'Internal Server Error'})
118113
vi.mocked(shopifyFetch).mockResolvedValue(response)
119114
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
120-
vi.mocked(clientId).mockReturnValue('clientId')
121115

122116
// When/Then
123117
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError(
@@ -134,7 +128,6 @@ describe('requestDeviceAuthorization', () => {
134128
response.text = vi.fn().mockRejectedValue(new Error('Network error'))
135129
vi.mocked(shopifyFetch).mockResolvedValue(response)
136130
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
137-
vi.mocked(clientId).mockReturnValue('clientId')
138131

139132
// When/Then
140133
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrowError(

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {clientId} from './identity.js'
21
import {exchangeDeviceCodeForAccessToken} from './exchange.js'
32
import {IdentityToken} from './schema.js'
43
import {identityFqdn} from '../../../public/node/context/fqdn.js'
@@ -8,6 +7,7 @@ import {AbortError, BugError} from '../../../public/node/error.js'
87
import {isCloudEnvironment} from '../../../public/node/context/local.js'
98
import {isCI, openURL} from '../../../public/node/system.js'
109
import {isTTY, keypress} from '../../../public/node/ui.js'
10+
import {getIdentityClient} from '../clients/identity/instance.js'
1111
import {Response} from 'node-fetch'
1212

1313
export interface DeviceAuthorizationResponse {
@@ -31,7 +31,7 @@ export interface DeviceAuthorizationResponse {
3131
*/
3232
export async function requestDeviceAuthorization(scopes: string[]): Promise<DeviceAuthorizationResponse> {
3333
const fqdn = await identityFqdn()
34-
const identityClientId = clientId()
34+
const identityClientId = getIdentityClient().clientId()
3535
const queryParams = {client_id: identityClientId, scope: scopes.join(' ')}
3636
const url = `https://${fqdn}/oauth/device_authorization`
3737

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
refreshAccessToken,
99
requestAppToken,
1010
} from './exchange.js'
11-
import {applicationId, clientId} from './identity.js'
11+
import {applicationId} from './identity.js'
1212
import {IdentityToken} from './schema.js'
1313
import {shopifyFetch} from '../../../public/node/http.js'
1414
import {identityFqdn} from '../../../public/node/context/fqdn.js'
@@ -43,7 +43,6 @@ vi.mock('../../../public/node/context/fqdn.js')
4343
vi.mock('./identity')
4444

4545
beforeEach(() => {
46-
vi.mocked(clientId).mockReturnValue('clientId')
4746
vi.setSystemTime(currentDate)
4847
vi.mocked(applicationId).mockImplementation((api) => api)
4948
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
@@ -301,7 +300,7 @@ describe.each(tokenExchangeMethods)(
301300
expect(params.get('grant_type')).toBe(grantType)
302301
expect(params.get('requested_token_type')).toBe(accessTokenType)
303302
expect(params.get('subject_token_type')).toBe(accessTokenType)
304-
expect(params.get('client_id')).toBe('clientId')
303+
expect(params.get('client_id')).toBe('fbdb2649-e327-4907-8f67-908d24cfd7e3')
305304
expect(params.get('audience')).toBe(expectedApi)
306305
expect(params.get('scope')).toBe(expectedScopes.join(' '))
307306
expect(params.get('subject_token')).toBe(cliToken)

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {ApplicationToken, IdentityToken} from './schema.js'
2-
import {applicationId, clientId as getIdentityClientId} from './identity.js'
2+
import {applicationId} from './identity.js'
33
import {tokenExchangeScopes} from './scopes.js'
44
import {API} from '../api.js'
55
import {identityFqdn} from '../../../public/node/context/fqdn.js'
@@ -8,6 +8,7 @@ import {err, ok, Result} from '../../../public/node/result.js'
88
import {AbortError, BugError, ExtendableError} from '../../../public/node/error.js'
99
import {setLastSeenAuthMethod, setLastSeenUserIdAfterAuth} from '../session.js'
1010
import {nonRandomUUID} from '../../../public/node/crypto.js'
11+
import {getIdentityClient} from '../clients/identity/instance.js'
1112
import * as jose from 'jose'
1213

1314
export class InvalidGrantError extends ExtendableError {}
@@ -56,14 +57,14 @@ export async function exchangeAccessForApplicationTokens(
5657
* Given an expired access token, refresh it to get a new one.
5758
*/
5859
export async function refreshAccessToken(currentToken: IdentityToken): Promise<IdentityToken> {
59-
const clientId = getIdentityClientId()
60+
const clientId = getIdentityClient().clientId()
6061
const params = {
6162
grant_type: 'refresh_token',
6263
access_token: currentToken.accessToken,
6364
refresh_token: currentToken.refreshToken,
6465
client_id: clientId,
6566
}
66-
const tokenResult = await tokenRequest(params)
67+
const tokenResult = await getIdentityClient().tokenRequest(params)
6768
const value = tokenResult.mapError(tokenRequestErrorHandler).valueOrBug()
6869
return buildIdentityToken(value, currentToken.userId, currentToken.alias)
6970
}
@@ -141,15 +142,15 @@ type IdentityDeviceError = 'authorization_pending' | 'access_denied' | 'expired_
141142
export async function exchangeDeviceCodeForAccessToken(
142143
deviceCode: string,
143144
): Promise<Result<IdentityToken, IdentityDeviceError>> {
144-
const clientId = await getIdentityClientId()
145+
const clientId = getIdentityClient().clientId()
145146

146147
const params = {
147148
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
148149
device_code: deviceCode,
149150
client_id: clientId,
150151
}
151152

152-
const tokenResult = await tokenRequest(params)
153+
const tokenResult = await getIdentityClient().tokenRequest(params)
153154
if (tokenResult.isErr()) {
154155
return err(tokenResult.error.error as IdentityDeviceError)
155156
}
@@ -164,7 +165,7 @@ export async function requestAppToken(
164165
store?: string,
165166
): Promise<{[x: string]: ApplicationToken}> {
166167
const appId = applicationId(api)
167-
const clientId = await getIdentityClientId()
168+
const clientId = getIdentityClient().clientId()
168169

169170
const params = {
170171
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
@@ -181,7 +182,7 @@ export async function requestAppToken(
181182
if (api === 'admin' && store) {
182183
identifier = `${store}-${appId}`
183184
}
184-
const tokenResult = await tokenRequest(params)
185+
const tokenResult = await getIdentityClient().tokenRequest(params)
185186
const value = tokenResult.mapError(tokenRequestErrorHandler).valueOrBug()
186187
const appToken = buildApplicationToken(value)
187188
return {[identifier]: appToken}
@@ -221,7 +222,7 @@ export function tokenRequestErrorHandler({error, store}: {error: string; store?:
221222
return new AbortError(error)
222223
}
223224

224-
async function tokenRequest(params: {
225+
async function _tokenRequest(params: {
225226
[key: string]: string
226227
}): Promise<Result<TokenRequestResult, {error: string; store?: string}>> {
227228
const fqdn = await identityFqdn()

0 commit comments

Comments
 (0)