Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/redact-device-auth-debug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-kit': patch
---

Redact device authorization secrets from verbose debug logs.
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
})
Expand Down Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,7 +66,9 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise<Devi
throw new BugError(errorMessage)
}

outputDebug(outputContent`Received device authorization code: ${outputToken.json(jsonResult)}`)
outputDebug(
outputContent`Received device authorization response: ${outputToken.json(redactDeviceAuthorizationResponse(jsonResult))}`,
)
if (!jsonResult.device_code || !jsonResult.verification_uri_complete) {
throw new BugError('Failed to start authorization process')
}
Expand Down Expand Up @@ -108,6 +112,16 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise<Devi
}
}

function redactDeviceAuthorizationResponse(jsonResult: Record<string, unknown>): Record<string, unknown> {
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.
Expand Down
Loading