diff --git a/packages/store/src/cli/services/store/auth/index.test.ts b/packages/store/src/cli/services/store/auth/index.test.ts index 709515377e6..4969769e491 100644 --- a/packages/store/src/cli/services/store/auth/index.test.ts +++ b/packages/store/src/cli/services/store/auth/index.test.ts @@ -304,10 +304,47 @@ describe('store auth service', () => { expect(presenter.openingBrowser).toHaveBeenCalledOnce() expect(presenter.manualAuthUrl).toHaveBeenCalledWith( expect.stringContaining('https://shop.myshopify.com/admin/oauth/authorize?'), + {sensitive: false}, ) expect(presenter.success).toHaveBeenCalledWith(result) }) + test('authenticateStoreWithApp marks manual auth URL as sensitive when signup JWT is present', async () => { + const openURL = vi.fn().mockResolvedValue(false) + const presenter = { + openingBrowser: vi.fn(), + manualAuthUrl: vi.fn(), + success: vi.fn(), + } + const waitForStoreAuthCodeMock = vi.fn().mockImplementation(async (options) => { + await options.onListening?.() + return 'abc123' + }) + + await authenticateStoreWithApp( + { + store: 'shop.myshopify.com', + scopes: 'read_products', + signup: 'signed.signup.jwt', + }, + { + openURL, + waitForStoreAuthCode: waitForStoreAuthCodeMock, + exchangeStoreAuthCodeForToken: vi.fn().mockResolvedValue({ + access_token: 'token', + scope: 'read_products', + expires_in: 86400, + associated_user: {id: 42, email: 'test@example.com'}, + }), + presenter, + }, + ) + + expect(presenter.manualAuthUrl).toHaveBeenCalledWith(expect.stringContaining('signup=signed.signup.jwt'), { + sensitive: true, + }) + }) + test('authenticateStoreWithApp records fqdn metadata before resolving existing scopes', async () => { await expect( authenticateStoreWithApp( diff --git a/packages/store/src/cli/services/store/auth/index.ts b/packages/store/src/cli/services/store/auth/index.ts index 9a1dc4c9f8e..f342ae5074b 100644 --- a/packages/store/src/cli/services/store/auth/index.ts +++ b/packages/store/src/cli/services/store/auth/index.ts @@ -76,7 +76,7 @@ export async function authenticateStoreWithApp( ...bootstrap.waitForAuthCodeOptions, onListening: async () => { const opened = await resolvedDependencies.openURL(authorizationUrl) - if (!opened) resolvedDependencies.presenter.manualAuthUrl(authorizationUrl) + if (!opened) resolvedDependencies.presenter.manualAuthUrl(authorizationUrl, {sensitive: Boolean(input.signup)}) }, }) const tokenResponse = await bootstrap.exchangeCodeForToken(code) diff --git a/packages/store/src/cli/services/store/auth/result.test.ts b/packages/store/src/cli/services/store/auth/result.test.ts index 06f7ae1fc64..0e820ea1fcb 100644 --- a/packages/store/src/cli/services/store/auth/result.test.ts +++ b/packages/store/src/cli/services/store/auth/result.test.ts @@ -103,4 +103,22 @@ describe('store auth presenter', () => { expect(streams.stdout()).toContain('"store": "shop.myshopify.com"') expect(streams.stdout()).not.toContain('Authenticated') }) + + test('does not print manual auth URL output when marked sensitive', () => { + const output = mockAndCaptureOutput() + const presenter = createStoreAuthPresenter('text') + + presenter.manualAuthUrl('https://shop.myshopify.com/admin/oauth/authorize?client_id=test&secret=sensitive', { + sensitive: true, + }) + + expect(output.info()).toContain( + 'Browser did not open automatically. The manual authorization URL contains sensitive credentials and was not printed.', + ) + expect(output.info()).toContain( + 'Run this command again in an environment where Shopify CLI can open a browser automatically.', + ) + expect(output.info()).not.toContain('secret=sensitive') + expect(output.info()).not.toContain('https://shop.myshopify.com/admin/oauth/authorize') + }) }) diff --git a/packages/store/src/cli/services/store/auth/result.ts b/packages/store/src/cli/services/store/auth/result.ts index 58098a7c4f4..db83df4a277 100644 --- a/packages/store/src/cli/services/store/auth/result.ts +++ b/packages/store/src/cli/services/store/auth/result.ts @@ -19,9 +19,13 @@ export interface StoreAuthResult { type StoreAuthOutputFormat = 'text' | 'json' +interface ManualAuthUrlOptions { + sensitive?: boolean +} + export interface StoreAuthPresenter { openingBrowser: () => void - manualAuthUrl: (authorizationUrl: string) => void + manualAuthUrl: (authorizationUrl: string, options?: ManualAuthUrlOptions) => void success: (result: StoreAuthResult) => void } @@ -47,7 +51,16 @@ function displayStoreAuthOpeningBrowser(): void { outputInfo('') } -function displayStoreAuthManualAuthUrl(authorizationUrl: string): void { +function displayStoreAuthManualAuthUrl(authorizationUrl: string, options: ManualAuthUrlOptions = {}): void { + if (options.sensitive) { + outputInfo( + 'Browser did not open automatically. The manual authorization URL contains sensitive credentials and was not printed.', + ) + outputInfo('Run this command again in an environment where Shopify CLI can open a browser automatically.') + outputInfo('') + return + } + outputInfo('Browser did not open automatically. Open this URL manually:') outputInfo(outputContent`${outputToken.link(authorizationUrl)}`) outputInfo('')