diff --git a/.changeset/redact-device-auth-debug.md b/.changeset/redact-device-auth-debug.md new file mode 100644 index 00000000000..9dd141a5dfc --- /dev/null +++ b/.changeset/redact-device-auth-debug.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Redact device authorization secrets from verbose debug logs. diff --git a/packages/cli-kit/src/private/node/session/device-authorization.test.ts b/packages/cli-kit/src/private/node/session/device-authorization.test.ts index 32349ba0ee2..b4d2d869265 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.test.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.test.ts @@ -12,6 +12,7 @@ import {isTTY} from '../../../public/node/ui.js' import {err, ok} from '../../../public/node/result.js' import {AbortError} from '../../../public/node/error.js' import {isCI} from '../../../public/node/system.js' +import {clearCollectedLogs, collectedLogs} from '../../../public/node/output.js' import {beforeEach, describe, expect, test, vi} from 'vitest' import {Response} from 'node-fetch' @@ -24,6 +25,7 @@ vi.mock('./exchange.js') vi.mock('../../../public/node/system.js') beforeEach(() => { + clearCollectedLogs() vi.mocked(isTTY).mockReturnValue(true) vi.mocked(isCI).mockReturnValue(false) }) @@ -66,6 +68,30 @@ describe('requestDeviceAuthorization', () => { expect(got).toEqual(dataExpected) }) + test('redacts sensitive device authorization fields from debug output', async () => { + // Given + const sensitiveData = { + ...data, + device_code: 'secret-device-code', + user_code: 'secret-user-code', + verification_uri_complete: 'https://accounts.shopify.com/activate?user_code=secret-user-code', + } + const response = new Response(JSON.stringify(sensitiveData)) + vi.mocked(shopifyFetch).mockResolvedValue(response) + vi.mocked(identityFqdn).mockResolvedValue('fqdn.com') + vi.mocked(clientId).mockReturnValue('clientId') + + // When + await requestDeviceAuthorization(['scope1', 'scope2']) + + // Then + const debugOutput = collectedLogs.debug?.join('\n') ?? '' + expect(debugOutput).toContain('Received device authorization response') + expect(debugOutput).toContain('****') + expect(debugOutput).not.toContain('secret-device-code') + expect(debugOutput).not.toContain('secret-user-code') + }) + test('when the response is not valid JSON, throw an error with context', async () => { // Given const response = new Response('not valid JSON') diff --git a/packages/cli-kit/src/private/node/session/device-authorization.ts b/packages/cli-kit/src/private/node/session/device-authorization.ts index 14e8e367a9f..5df163e7b63 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.ts @@ -11,6 +11,8 @@ import {isTTY, keypress} from '../../../public/node/ui.js' import {Response} from 'node-fetch' +const DEVICE_AUTHORIZATION_DEBUG_REDACTED_FIELDS = ['device_code', 'user_code', 'verification_uri_complete'] as const + export interface DeviceAuthorizationResponse { deviceCode: string userCode: string @@ -64,7 +66,9 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise): Record { + const redactedResult = {...jsonResult} + + for (const field of DEVICE_AUTHORIZATION_DEBUG_REDACTED_FIELDS) { + if (redactedResult[field]) redactedResult[field] = '****' + } + + return redactedResult +} + /** * Poll the Oauth token endpoint with the device code obtained from a DeviceAuthorizationResponse. * The endpoint will return `authorization_pending` until the user completes the auth flow in the browser.