Skip to content
This repository was archived by the owner on Apr 11, 2024. It is now read-only.

Commit 447af77

Browse files
authored
Merge pull request #824 from Shopify/abh/oauth
Adds check for Google Crawler attempting to crawl Auth Routes
2 parents 332b2fb + c959064 commit 447af77

File tree

8 files changed

+80
-4
lines changed

8 files changed

+80
-4
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/shopify-api': patch
3+
---
4+
5+
Adds check for Google's Crawler in the authorization functions to prevent CookieNotFound error loops. Fixes #686

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ bundle/
1313
*.js
1414
*.js.map
1515
*.tgz
16+
.vscode/
17+

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"cSpell.words": ["isbot"]
3+
}

lib/auth/oauth/__tests__/oauth.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,19 @@ describe('beginAuth', () => {
141141
);
142142
});
143143

144+
test('response with a 410 when the request is a bot', async () => {
145+
request.headers['User-Agent'] = 'Googlebot';
146+
147+
const response: NormalizedResponse = await shopify.auth.begin({
148+
shop,
149+
isOnline: true,
150+
callbackPath: '/some-callback',
151+
rawRequest: request,
152+
});
153+
154+
expect(response.statusCode).toBe(410);
155+
});
156+
144157
test('fails to start if the app is private', () => {
145158
shopify.config.isCustomStoreApp = true;
146159

@@ -260,7 +273,9 @@ describe('callback', () => {
260273
queueMockResponse(JSON.stringify(successResponse));
261274

262275
const callbackResponse = await shopify.auth.callback({rawRequest: request});
263-
276+
if (!callbackResponse) {
277+
fail('Callback response is undefined');
278+
}
264279
const expectedId = `offline_${shop}`;
265280
const responseCookies = Cookies.parseCookies(
266281
callbackResponse.headers['Set-Cookie'],
@@ -521,6 +536,22 @@ describe('callback', () => {
521536
expect(responseCookies.shopify_app_session).toBeUndefined();
522537
});
523538

539+
test('callback throws an error when the request is done by a bot', async () => {
540+
const botRequest = {
541+
method: 'GET',
542+
url: 'https://my-test-app.myshopify.io/totally-real-request',
543+
headers: {
544+
'User-Agent': 'Googlebot',
545+
},
546+
} as NormalizedRequest;
547+
548+
await expect(
549+
shopify.auth.callback({
550+
rawRequest: botRequest,
551+
}),
552+
).rejects.toThrow(ShopifyErrors.BotActivityDetected);
553+
});
554+
524555
test('properly updates the OAuth cookie for offline, non-embedded apps', async () => {
525556
shopify.config.isEmbeddedApp = false;
526557

lib/auth/oauth/oauth.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {v4 as uuidv4} from 'uuid';
2+
import isbot from 'isbot';
23

34
import ProcessedQuery from '../../utils/processed-query';
45
import {ConfigInterface} from '../../base-types';
@@ -18,8 +19,9 @@ import {
1819
AdapterHeaders,
1920
Cookies,
2021
NormalizedResponse,
22+
NormalizedRequest,
2123
} from '../../../runtime/http';
22-
import {logger} from '../../logger';
24+
import {logger, ShopifyLogger} from '../../logger';
2325

2426
import {
2527
SESSION_COOKIE_NAME,
@@ -39,6 +41,18 @@ export interface CallbackResponse<T = AdapterHeaders> {
3941
session: Session;
4042
}
4143

44+
interface BotLog {
45+
request: NormalizedRequest;
46+
log: ShopifyLogger;
47+
func: string;
48+
}
49+
50+
const logForBot = ({request, log, func}: BotLog) => {
51+
log.debug(`Possible bot request to auth ${func}: `, {
52+
userAgent: request.headers['User-Agent'],
53+
});
54+
};
55+
4256
export function begin(config: ConfigInterface) {
4357
return async ({
4458
shop,
@@ -54,10 +68,15 @@ export function begin(config: ConfigInterface) {
5468
const log = logger(config);
5569
log.info('Beginning OAuth', {shop, isOnline, callbackPath});
5670

57-
const cleanShop = sanitizeShop(config)(shop, true)!;
5871
const request = await abstractConvertRequest(adapterArgs);
5972
const response = await abstractConvertIncomingResponse(adapterArgs);
6073

74+
if (isbot(request.headers['User-Agent'])) {
75+
logForBot({request, log, func: 'begin'});
76+
response.statusCode = 410;
77+
return abstractConvertResponse(response, adapterArgs);
78+
}
79+
6180
const cookies = new Cookies(request, response, {
6281
keys: [config.apiSecretKey],
6382
secure: true,
@@ -82,6 +101,7 @@ export function begin(config: ConfigInterface) {
82101
const processedQuery = new ProcessedQuery();
83102
processedQuery.putAll(query);
84103

104+
const cleanShop = sanitizeShop(config)(shop, true)!;
85105
const redirectUrl = `https://${cleanShop}/admin/oauth/authorize${processedQuery.stringify()}`;
86106
response.statusCode = 302;
87107
response.statusText = 'Found';
@@ -116,9 +136,17 @@ export function callback(config: ConfigInterface) {
116136
).searchParams;
117137
const shop = query.get('shop')!;
118138

139+
const response = {} as NormalizedResponse;
140+
if (isbot(request.headers['User-Agent'])) {
141+
logForBot({request, log, func: 'callback'});
142+
throw new ShopifyErrors.BotActivityDetected(
143+
'Invalid OAuth callback initiated by bot',
144+
);
145+
}
146+
119147
log.info('Completing OAuth', {shop});
120148

121-
const cookies = new Cookies(request, {} as NormalizedResponse, {
149+
const cookies = new Cookies(request, response, {
122150
keys: [config.apiSecretKey],
123151
secure: true,
124152
});

lib/error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export class GraphqlQueryError extends ShopifyError {
8787
}
8888

8989
export class InvalidOAuthError extends ShopifyError {}
90+
export class BotActivityDetected extends ShopifyError {}
9091
export class CookieNotFound extends ShopifyError {}
9192
export class InvalidSession extends ShopifyError {}
9293

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"dependencies": {
7777
"@shopify/network": "^3.2.1",
7878
"compare-versions": "^5.0.3",
79+
"isbot": "^3.6.10",
7980
"jose": "^4.9.1",
8081
"node-fetch": "^2.6.1",
8182
"tslib": "^2.0.3",

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4109,6 +4109,11 @@ is-windows@^1.0.0:
41094109
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
41104110
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
41114111

4112+
isbot@^3.6.10:
4113+
version "3.6.10"
4114+
resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.10.tgz#7b66334e81794f0461794debb567975cf08eaf2b"
4115+
integrity sha512-+I+2998oyP4oW9+OTQD8TS1r9P6wv10yejukj+Ksj3+UR5pUhsZN3f8W7ysq0p1qxpOVNbl5mCuv0bCaF8y5iQ==
4116+
41124117
isexe@^2.0.0:
41134118
version "2.0.0"
41144119
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"

0 commit comments

Comments
 (0)