Skip to content

Commit 3518185

Browse files
Merge pull request #8427 from Shopify/redact-store-auth-manual-url
Avoid printing sensitive store auth URLs
2 parents 3bb4bc7 + 2cce6bf commit 3518185

4 files changed

Lines changed: 113 additions & 3 deletions

File tree

packages/store/src/cli/services/store/auth/index.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,10 +304,82 @@ describe('store auth service', () => {
304304
expect(presenter.openingBrowser).toHaveBeenCalledOnce()
305305
expect(presenter.manualAuthUrl).toHaveBeenCalledWith(
306306
expect.stringContaining('https://shop.myshopify.com/admin/oauth/authorize?'),
307+
{sensitive: false},
307308
)
308309
expect(presenter.success).toHaveBeenCalledWith(result)
309310
})
310311

312+
test('authenticateStoreWithApp marks manual auth URL as sensitive when signup JWT is present', async () => {
313+
const openURL = vi.fn().mockResolvedValue(false)
314+
const presenter = {
315+
openingBrowser: vi.fn(),
316+
manualAuthUrl: vi.fn(),
317+
success: vi.fn(),
318+
}
319+
const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => {
320+
await options.onListening?.()
321+
return 'abc123'
322+
})
323+
324+
await expect(
325+
authenticateStoreWithApp(
326+
{
327+
store: 'shop.myshopify.com',
328+
scopes: 'read_products',
329+
signup: 'signed.signup.jwt',
330+
},
331+
{
332+
openURL,
333+
waitForStoreAuthCode: waitForStoreAuthCodeMock,
334+
exchangeStoreAuthCodeForToken: vi.fn().mockResolvedValue({
335+
access_token: 'token',
336+
scope: 'read_products',
337+
expires_in: 86400,
338+
associated_user: {id: 42, email: 'test@example.com'},
339+
}),
340+
presenter,
341+
},
342+
),
343+
).rejects.toThrow()
344+
345+
expect(presenter.manualAuthUrl).toHaveBeenCalledWith(expect.stringContaining('signup=signed.signup.jwt'), {
346+
sensitive: true,
347+
})
348+
})
349+
350+
test('authenticateStoreWithApp fails immediately instead of waiting for a callback that cannot arrive', async () => {
351+
const openURL = vi.fn().mockResolvedValue(false)
352+
const presenter = {
353+
openingBrowser: vi.fn(),
354+
manualAuthUrl: vi.fn(),
355+
success: vi.fn(),
356+
}
357+
const exchangeStoreAuthCodeForToken = vi.fn()
358+
const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => {
359+
await options.onListening?.()
360+
return 'abc123'
361+
})
362+
363+
await expect(
364+
authenticateStoreWithApp(
365+
{
366+
store: 'shop.myshopify.com',
367+
scopes: 'read_products',
368+
signup: 'signed.signup.jwt',
369+
},
370+
{
371+
openURL,
372+
waitForStoreAuthCode: waitForStoreAuthCodeMock,
373+
exchangeStoreAuthCodeForToken,
374+
presenter,
375+
},
376+
),
377+
).rejects.toThrow("Authentication can't continue without a browser.")
378+
379+
expect(exchangeStoreAuthCodeForToken).not.toHaveBeenCalled()
380+
expect(presenter.success).not.toHaveBeenCalled()
381+
})
382+
311383
test('authenticateStoreWithApp records fqdn metadata before resolving existing scopes', async () => {
312384
await expect(
313385
authenticateStoreWithApp(

packages/store/src/cli/services/store/auth/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ export async function authenticateStoreWithApp(
7676
...bootstrap.waitForAuthCodeOptions,
7777
onListening: async () => {
7878
const opened = await resolvedDependencies.openURL(authorizationUrl)
79-
if (!opened) resolvedDependencies.presenter.manualAuthUrl(authorizationUrl)
79+
if (opened) return
80+
81+
const sensitive = Boolean(input.signup)
82+
resolvedDependencies.presenter.manualAuthUrl(authorizationUrl, {sensitive})
83+
84+
// A withheld URL never reaches the browser, so the callback this server is waiting for cannot
85+
// arrive. Returning here would leave the command idle until the timeout elapses.
86+
if (sensitive) throw new AbortError("Authentication can't continue without a browser.")
8087
},
8188
})
8289
const tokenResponse = await bootstrap.exchangeCodeForToken(code)

packages/store/src/cli/services/store/auth/result.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,22 @@ describe('store auth presenter', () => {
103103
expect(streams.stdout()).toContain('"store": "shop.myshopify.com"')
104104
expect(streams.stdout()).not.toContain('Authenticated')
105105
})
106+
107+
test('does not print manual auth URL output when marked sensitive', () => {
108+
const output = mockAndCaptureOutput()
109+
const presenter = createStoreAuthPresenter('text')
110+
111+
presenter.manualAuthUrl('https://shop.myshopify.com/admin/oauth/authorize?client_id=test&secret=sensitive', {
112+
sensitive: true,
113+
})
114+
115+
expect(output.info()).toContain(
116+
'Browser did not open automatically. The manual authorization URL contains sensitive credentials and was not printed.',
117+
)
118+
expect(output.info()).toContain(
119+
'Run this command again in an environment where Shopify CLI can open a browser automatically.',
120+
)
121+
expect(output.info()).not.toContain('secret=sensitive')
122+
expect(output.info()).not.toContain('https://shop.myshopify.com/admin/oauth/authorize')
123+
})
106124
})

packages/store/src/cli/services/store/auth/result.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ export interface StoreAuthResult {
1919

2020
type StoreAuthOutputFormat = 'text' | 'json'
2121

22+
interface ManualAuthUrlOptions {
23+
sensitive?: boolean
24+
}
25+
2226
export interface StoreAuthPresenter {
2327
openingBrowser: () => void
24-
manualAuthUrl: (authorizationUrl: string) => void
28+
manualAuthUrl: (authorizationUrl: string, options?: ManualAuthUrlOptions) => void
2529
success: (result: StoreAuthResult) => void
2630
}
2731

@@ -47,7 +51,16 @@ function displayStoreAuthOpeningBrowser(): void {
4751
outputInfo('')
4852
}
4953

50-
function displayStoreAuthManualAuthUrl(authorizationUrl: string): void {
54+
function displayStoreAuthManualAuthUrl(authorizationUrl: string, options: ManualAuthUrlOptions = {}): void {
55+
if (options.sensitive) {
56+
outputInfo(
57+
'Browser did not open automatically. The manual authorization URL contains sensitive credentials and was not printed.',
58+
)
59+
outputInfo('Run this command again in an environment where Shopify CLI can open a browser automatically.')
60+
outputInfo('')
61+
return
62+
}
63+
5164
outputInfo('Browser did not open automatically. Open this URL manually:')
5265
outputInfo(outputContent`${outputToken.link(authorizationUrl)}`)
5366
outputInfo('')

0 commit comments

Comments
 (0)