Skip to content

Commit d702bd2

Browse files
committed
fix: fixed handling successful enterprise validator response (INVALID_REASON_UNSPECIFIED).
1 parent 5ddce74 commit d702bd2

8 files changed

+160
-28
lines changed

src/google-recaptcha.module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export class GoogleRecaptchaModule {
154154
return {
155155
provide: RECAPTCHA_OPTIONS,
156156
useFactory: options.useFactory,
157-
inject: options.inject || [],
157+
inject: options.inject,
158158
};
159159
}
160160

src/services/enterprise-reason.transformer.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,27 @@ import { ErrorCode } from '../enums/error-code';
44

55
@Injectable()
66
export class EnterpriseReasonTransformer {
7-
transform(errCode: GoogleRecaptchaEnterpriseReason): ErrorCode {
7+
transform(errCode: GoogleRecaptchaEnterpriseReason): ErrorCode | null {
88
switch (errCode) {
99
case GoogleRecaptchaEnterpriseReason.BrowserError:
1010
return ErrorCode.BrowserError;
1111

12-
case GoogleRecaptchaEnterpriseReason.UnknownInvalidReason:
13-
return ErrorCode.UnknownError;
14-
1512
case GoogleRecaptchaEnterpriseReason.SiteMismatch:
1613
return ErrorCode.SiteMismatch;
1714

1815
case GoogleRecaptchaEnterpriseReason.Expired:
1916
case GoogleRecaptchaEnterpriseReason.Dupe:
2017
return ErrorCode.TimeoutOrDuplicate;
2118

19+
case GoogleRecaptchaEnterpriseReason.UnknownInvalidReason:
2220
case GoogleRecaptchaEnterpriseReason.Malformed:
2321
return ErrorCode.InvalidInputResponse;
2422

2523
case GoogleRecaptchaEnterpriseReason.Missing:
2624
return ErrorCode.MissingInputResponse;
2725

2826
case GoogleRecaptchaEnterpriseReason.InvalidReasonUnspecified:
29-
return ErrorCode.UnknownError;
27+
return null;
3028

3129
default:
3230
return ErrorCode.UnknownError;

src/services/validators/google-recaptcha-enterprise.validator.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -35,28 +35,30 @@ export class GoogleRecaptchaEnterpriseValidator extends AbstractGoogleRecaptchaV
3535
const errors: ErrorCode[] = [];
3636
let success = result?.tokenProperties?.valid || false;
3737

38-
if (!success) {
39-
errors.push(ErrorCode.InvalidInputResponse);
40-
}
41-
42-
if (errorDetails) {
43-
errors.push(ErrorCode.UnknownError);
44-
}
38+
if (!errorDetails) {
39+
if (result.tokenProperties) {
40+
if (result.tokenProperties.invalidReason) {
41+
const invalidReasonCode = this.enterpriseReasonTransformer.transform(result.tokenProperties.invalidReason);
42+
43+
if (invalidReasonCode) {
44+
errors.push(invalidReasonCode);
45+
}
46+
}
4547

46-
if (result?.tokenProperties) {
47-
if (result.tokenProperties.invalidReason) {
48-
errors.push(this.enterpriseReasonTransformer.transform(result.tokenProperties.invalidReason));
48+
if (success && !this.isValidAction(result.tokenProperties.action, options)) {
49+
success = false;
50+
errors.push(ErrorCode.ForbiddenAction);
51+
}
4952
}
5053

51-
if (!this.isValidAction(result.tokenProperties.action, options)) {
54+
if (result.riskAnalysis && !this.isValidScore(result.riskAnalysis.score, options.score)) {
5255
success = false;
53-
errors.push(ErrorCode.ForbiddenAction);
56+
errors.push(ErrorCode.LowScore);
5457
}
5558
}
5659

57-
if (result.riskAnalysis && !this.isValidScore(result.riskAnalysis.score, options.score)) {
58-
success = false;
59-
errors.push(ErrorCode.LowScore);
60+
if (!success && !errors.length) {
61+
errorDetails ? errors.push(ErrorCode.UnknownError) : errors.push(ErrorCode.InvalidInputResponse);
6062
}
6163

6264
return new RecaptchaVerificationResult({

test/assets/test-error-filter.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
2+
import { GoogleRecaptchaException } from '../../src';
3+
import { Response } from 'express';
4+
5+
@Catch(Error)
6+
export class TestErrorFilter implements ExceptionFilter {
7+
catch(exception: Error, host: ArgumentsHost): void {
8+
const res: Response = host.switchToHttp().getResponse();
9+
10+
if (exception instanceof GoogleRecaptchaException) {
11+
res.status(exception.getStatus()).send({
12+
errorCodes: exception.errorCodes,
13+
});
14+
15+
return;
16+
}
17+
18+
res.status(500).send({
19+
name: exception.name,
20+
message: exception.message,
21+
stack: exception.stack,
22+
});
23+
}
24+
}

test/enterprise-reason.transformer.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ describe('EnterpriseReasonTransformer', () => {
66
const transformer = new EnterpriseReasonTransformer();
77

88
const expectedMap: Map<GoogleRecaptchaEnterpriseReason, ErrorCode> = new Map<GoogleRecaptchaEnterpriseReason, ErrorCode>([
9-
[GoogleRecaptchaEnterpriseReason.InvalidReasonUnspecified, ErrorCode.UnknownError],
10-
[GoogleRecaptchaEnterpriseReason.UnknownInvalidReason, ErrorCode.UnknownError],
9+
[GoogleRecaptchaEnterpriseReason.InvalidReasonUnspecified, null],
10+
[GoogleRecaptchaEnterpriseReason.UnknownInvalidReason, ErrorCode.InvalidInputResponse],
1111
[GoogleRecaptchaEnterpriseReason.Malformed, ErrorCode.InvalidInputResponse],
1212
[GoogleRecaptchaEnterpriseReason.Expired, ErrorCode.TimeoutOrDuplicate],
1313
[GoogleRecaptchaEnterpriseReason.Dupe, ErrorCode.TimeoutOrDuplicate],

test/integrations/graphql/graphql-recaptcha-v2-v3.spec.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import { MockedRecaptchaApi } from '../../utils/mocked-recaptcha-api';
77
import { VerifyResponseV3 } from '../../../src/interfaces/verify-response';
88
import { TestHttp } from '../../utils/test-http';
99
import { IncomingMessage } from 'http';
10-
import { Args, Field, GraphQLModule, InputType, Mutation, ObjectType, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
10+
import { Args, Field, GraphQLModule, InputType, Mutation, ObjectType, Query, Resolver } from '@nestjs/graphql';
1111
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
1212
import * as path from 'path';
13-
import { GraphQLSchemaBuilder } from '@nestjs/graphql/dist/graphql-schema.builder';
14-
import { GraphQLSchema } from 'graphql';
1513

1614
@InputType()
1715
export class FeedbackInput {

test/integrations/http-recaptcha-enterprice.spec.ts

+99-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Controller, INestApplication, LiteralObject, Post } from '@nestjs/common';
2-
import { ClassificationReason, GoogleRecaptchaModule, Recaptcha, RecaptchaResult, RecaptchaVerificationResult } from '../../src';
2+
import { ClassificationReason, ErrorCode, GoogleRecaptchaModule, Recaptcha, RecaptchaResult, RecaptchaVerificationResult } from '../../src';
33
import { Test, TestingModule } from '@nestjs/testing';
44
import { Request } from 'express';
55
import { RECAPTCHA_HTTP_SERVICE } from '../../src/provider.declarations';
@@ -9,6 +9,7 @@ import { VerifyResponseV2 } from '../../src/interfaces/verify-response';
99
import { TestHttp } from '../utils/test-http';
1010
import { VerifyResponseEnterprise } from '../../src/interfaces/verify-response-enterprise';
1111
import { GoogleRecaptchaEnterpriseReason } from '../../src/enums/google-recaptcha-enterprise-reason';
12+
import { TestErrorFilter } from '../assets/test-error-filter';
1213

1314
@Controller('test')
1415
class TestController {
@@ -64,6 +65,8 @@ describe('HTTP Recaptcha Enterprise', () => {
6465

6566
app = module.createNestApplication();
6667

68+
app.useGlobalFilters(new TestErrorFilter());
69+
6770
await app.init();
6871

6972
http = new TestHttp(app.getHttpServer());
@@ -108,6 +111,42 @@ describe('HTTP Recaptcha Enterprise', () => {
108111
expect(res.body.success).toBe(true);
109112
});
110113

114+
test('Enterprise token malformed', async () => {
115+
mockedRecaptchaApi.addResponse<VerifyResponseEnterprise>('test_enterprise_token_malformed', {
116+
name: 'name',
117+
event: {
118+
userIpAddress: '0.0.0.0',
119+
siteKey: 'siteKey',
120+
userAgent: 'UA',
121+
token: '',
122+
hashedAccountId: '',
123+
expectedAction: 'Submit',
124+
},
125+
tokenProperties: {
126+
valid: false,
127+
invalidReason: GoogleRecaptchaEnterpriseReason.Malformed,
128+
hostname: '',
129+
action: '',
130+
createTime: '1970-01-01T00:00:00Z',
131+
},
132+
});
133+
134+
const res: request.Response = await http.post(
135+
'/test/submit',
136+
{},
137+
{
138+
headers: {
139+
Recaptcha: 'test_enterprise_token_malformed',
140+
},
141+
}
142+
);
143+
144+
expect(res.statusCode).toBe(400);
145+
expect(res.body.errorCodes).toBeDefined();
146+
expect(res.body.errorCodes.length).toBe(1);
147+
expect(res.body.errorCodes[0]).toBe(ErrorCode.InvalidInputResponse);
148+
});
149+
111150
test('Enterprise without token properties', async () => {
112151
mockedRecaptchaApi.addResponse<VerifyResponseEnterprise>('test_enterprise_without_token_props', {
113152
name: 'name',
@@ -132,6 +171,10 @@ describe('HTTP Recaptcha Enterprise', () => {
132171
);
133172

134173
expect(res.statusCode).toBe(400);
174+
175+
expect(res.body.errorCodes).toBeDefined();
176+
expect(res.body.errorCodes.length).toBe(1);
177+
expect(res.body.errorCodes[0]).toBe(ErrorCode.InvalidInputResponse);
135178
});
136179

137180
test('Enterprise API error', async () => {
@@ -150,6 +193,10 @@ describe('HTTP Recaptcha Enterprise', () => {
150193
);
151194

152195
expect(res.statusCode).toBe(500);
196+
197+
expect(res.body.errorCodes).toBeDefined();
198+
expect(res.body.errorCodes.length).toBe(1);
199+
expect(res.body.errorCodes[0]).toBe(ErrorCode.UnknownError);
153200
});
154201

155202
test('Enterprise Network error', async () => {
@@ -168,6 +215,10 @@ describe('HTTP Recaptcha Enterprise', () => {
168215
);
169216

170217
expect(res.statusCode).toBe(500);
218+
219+
expect(res.body.errorCodes).toBeDefined();
220+
expect(res.body.errorCodes.length).toBe(1);
221+
expect(res.body.errorCodes[0]).toBe(ErrorCode.NetworkError);
171222
});
172223

173224
test('Enterprise Expired token', async () => {
@@ -205,6 +256,10 @@ describe('HTTP Recaptcha Enterprise', () => {
205256
);
206257

207258
expect(res.statusCode).toBe(400);
259+
expect(res.body.errorCodes).toBeDefined();
260+
expect(res.body.errorCodes.length).toBe(2);
261+
expect(res.body.errorCodes[0]).toBe(ErrorCode.TimeoutOrDuplicate);
262+
expect(res.body.errorCodes[1]).toBe(ErrorCode.ForbiddenAction);
208263
});
209264

210265
test('Enterprise Invalid action', async () => {
@@ -277,5 +332,48 @@ describe('HTTP Recaptcha Enterprise', () => {
277332
);
278333

279334
expect(res.statusCode).toBe(400);
335+
expect(res.body.errorCodes).toBeDefined();
336+
expect(res.body.errorCodes.length).toBe(1);
337+
expect(res.body.errorCodes[0]).toBe(ErrorCode.LowScore);
338+
});
339+
340+
test('Enterprise Invalid reason unspecified + low score', async () => {
341+
mockedRecaptchaApi.addResponse<VerifyResponseEnterprise>('test_enterprise_inv_reason_unspecified_low_score', {
342+
name: 'name',
343+
event: {
344+
token: 'token',
345+
siteKey: 'siteKey',
346+
userAgent: '',
347+
userIpAddress: '',
348+
expectedAction: 'Submit',
349+
hashedAccountId: '',
350+
},
351+
riskAnalysis: {
352+
score: 0.6,
353+
reasons: [],
354+
},
355+
tokenProperties: {
356+
valid: true,
357+
invalidReason: GoogleRecaptchaEnterpriseReason.InvalidReasonUnspecified,
358+
hostname: 'localhost',
359+
action: 'Submit',
360+
createTime: '2022-09-07T19:53:55.566Z',
361+
},
362+
});
363+
364+
const res: request.Response = await http.post(
365+
'/test/submit',
366+
{},
367+
{
368+
headers: {
369+
Recaptcha: 'test_enterprise_inv_reason_unspecified_low_score',
370+
},
371+
}
372+
);
373+
374+
expect(res.statusCode).toBe(400);
375+
expect(res.body.errorCodes).toBeDefined();
376+
expect(res.body.errorCodes.length).toBe(1);
377+
expect(res.body.errorCodes[0]).toBe(ErrorCode.LowScore);
280378
});
281379
});

test/integrations/http-recaptcha-v2-v3.spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Controller, INestApplication, LiteralObject, Post } from '@nestjs/common';
2-
import { GoogleRecaptchaModule, Recaptcha, RecaptchaResult, RecaptchaVerificationResult } from '../../src';
2+
import { ErrorCode, GoogleRecaptchaModule, Recaptcha, RecaptchaResult, RecaptchaVerificationResult } from '../../src';
33
import { Test, TestingModule } from '@nestjs/testing';
44
import { Request } from 'express';
55
import { RECAPTCHA_HTTP_SERVICE } from '../../src/provider.declarations';
66
import * as request from 'supertest';
77
import { MockedRecaptchaApi } from '../utils/mocked-recaptcha-api';
88
import { VerifyResponseV2, VerifyResponseV3 } from '../../src/interfaces/verify-response';
99
import { TestHttp } from '../utils/test-http';
10+
import { TestErrorFilter } from '../assets/test-error-filter';
1011

1112
@Controller('test')
1213
class TestController {
@@ -55,6 +56,8 @@ describe('HTTP Recaptcha V2 V3', () => {
5556

5657
app = module.createNestApplication();
5758

59+
app.useGlobalFilters(new TestErrorFilter());
60+
5861
await app.init();
5962

6063
http = new TestHttp(app.getHttpServer());
@@ -100,6 +103,9 @@ describe('HTTP Recaptcha V2 V3', () => {
100103
);
101104

102105
expect(res.statusCode).toBe(500);
106+
expect(res.body.errorCodes).toBeDefined();
107+
expect(res.body.errorCodes.length).toBe(1);
108+
expect(res.body.errorCodes[0]).toBe(ErrorCode.UnknownError);
103109
});
104110

105111
test('V2 Network error', async () => {
@@ -165,6 +171,9 @@ describe('HTTP Recaptcha V2 V3', () => {
165171
);
166172

167173
expect(res.statusCode).toBe(400);
174+
expect(res.body.errorCodes).toBeDefined();
175+
expect(res.body.errorCodes.length).toBe(1);
176+
expect(res.body.errorCodes[0]).toBe(ErrorCode.ForbiddenAction);
168177
});
169178

170179
test('V3 Low score', async () => {
@@ -188,5 +197,8 @@ describe('HTTP Recaptcha V2 V3', () => {
188197
);
189198

190199
expect(res.statusCode).toBe(400);
200+
expect(res.body.errorCodes).toBeDefined();
201+
expect(res.body.errorCodes.length).toBe(1);
202+
expect(res.body.errorCodes[0]).toBe(ErrorCode.LowScore);
191203
});
192204
});

0 commit comments

Comments
 (0)