Skip to content
Merged
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
8 changes: 6 additions & 2 deletions packages/cli/src/cli/services/store/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe('store auth service', () => {
).rejects.toThrow('OAuth callback state does not match the original request.')
})

test('waitForStoreAuthCode rejects when callback store does not match', async () => {
test('waitForStoreAuthCode rejects when callback store does not match and suggests the returned permanent domain', async () => {
const port = await getAvailablePort()
const params = callbackParams({shop: 'other-shop.myshopify.com'})

Expand All @@ -172,7 +172,11 @@ describe('store auth service', () => {
await response.text()
},
}),
).rejects.toThrow('OAuth callback store does not match the requested store.')
).rejects.toMatchObject({
message: 'OAuth callback store does not match the requested store.',
tryMessage:
'Shopify returned other-shop.myshopify.com during authentication. Re-run shopify store auth --store other-shop.myshopify.com --scopes <comma-separated-scopes> using the permanent store domain instead of an alias or vanity domain.',
})
})

test('waitForStoreAuthCode rejects when Shopify returns an OAuth error', async () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/cli/services/store/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,31 @@ export async function waitForStoreAuthCode({

const {searchParams} = requestUrl

const fail = (message: string) => {
const fail = (message: string, tryMessage?: string) => {
res.statusCode = 400
res.setHeader('Content-Type', 'text/html')
res.setHeader('Connection', 'close')
res.once('finish', () => settleWithError(new AbortError(message)))
res.once('finish', () => settleWithError(new AbortError(message, tryMessage)))
res.end(renderAuthCallbackPage('Authentication failed', message))
}

const returnedStore = searchParams.get('shop')
outputDebug(outputContent`Received OAuth callback for shop ${outputToken.raw(returnedStore ?? 'unknown')}`)

if (!returnedStore || normalizeStoreFqdn(returnedStore) !== normalizedStore) {
if (!returnedStore) {
fail('OAuth callback store does not match the requested store.')
return
}

const normalizedReturnedStore = normalizeStoreFqdn(returnedStore)
if (normalizedReturnedStore !== normalizedStore) {
fail(
'OAuth callback store does not match the requested store.',
`Shopify returned ${normalizedReturnedStore} during authentication. Re-run ${outputToken.genericShellCommand(`shopify store auth --store ${normalizedReturnedStore} --scopes <comma-separated-scopes>`).value} using the permanent store domain instead of an alias or vanity domain.`,
)
return
}

const returnedState = searchParams.get('state')
if (!returnedState || !constantTimeEqual(returnedState, state)) {
fail('OAuth callback state does not match the original request.')
Expand Down
Loading