Skip to content

Commit f0010e7

Browse files
authored
feat: add support for attack-protection bot-detection and captcha configurations (#1189)
* feat: enhance attack protection configuration - src/context/directory/handlers/attackProtection.ts: add botDetection and captcha properties to ParsedAttackProtection - src/context/directory/handlers/attackProtection.ts: update attackProtectionFiles to include botDetection and captcha file paths - src/context/directory/handlers/attackProtection.ts: load botDetection and captcha configurations in parse function - src/context/directory/handlers/attackProtection.ts: dump botDetection and captcha configurations in dump function - src/context/yaml/handlers/attackProtection.ts: include botDetection and captcha in ParsedAttackProtection - src/tools/auth0/handlers/attackProtection.ts: add schema definitions for botDetection and captcha - test/context/directory/attackProtection.test.js: add tests for botDetection and captcha configurations - test/context/yaml/attackProtection.test.js: add YAML tests for botDetection and captcha - test/tools/auth0/handlers/attackProtection.tests.js: mock botDetection and captcha in handler tests * feat: implement attack protection defaults and integration - src/context/defaults.ts: add attackProtectionDefaults function to mask sensitive captcha secrets. - src/context/directory/handlers/attackProtection.ts: integrate attackProtectionDefaults for parsing attack protection data. - src/context/yaml/handlers/attackProtection.ts: utilize attackProtectionDefaults to mask captcha configuration. - src/tools/auth0/handlers/attackProtection.ts: refactor CAPTCHA providers into a constant and update related logic. * feat: enhance attack protection types and defaults - src/context/defaults.ts: add type annotation for attackProtection parameter - src/context/directory/handlers/attackProtection.ts: update ParsedAttackProtection type to use AttackProtection - src/context/yaml/handlers/attackProtection.ts: update ParsedAttackProtection type to use AttackProtection - src/tools/auth0/handlers/attackProtection.ts: define AttackProtection type and update existing property type - src/types.ts: update attackProtection asset type to use AttackProtection * feat: refactor attack protection client calls - src/tools/auth0/handlers/attackProtection.ts: replace direct client reference with this.client for attack protection methods * feat: update auth0 dependency version - package.json: bump auth0 from ^4.34.0 to ^4.35.0 - package-lock.json: bump auth0 from 4.34.0 to 4.35.0 * feat: add attack protection configurations - examples/yaml/tenant.yaml: introduce attackProtection section with botDetection, captcha, breachedPasswordDetection, bruteForceProtection, and suspiciousIpThrottling settings - examples/directory/attack-protection/bot-detection.json: create bot detection configuration file - examples/directory/attack-protection/breached-password-detection.json: create breached password detection configuration file - examples/directory/attack-protection/brute-force-protection.json: create brute force protection configuration file - examples/directory/attack-protection/captcha.json: create captcha configuration file - examples/directory/attack-protection/suspicious-ip-throttling.json: create suspicious IP throttling configuration file * feat(tests): enhance attack protection tests - test/tools/auth0/handlers/attackProtection.tests.js: add test for handling 403 error when fetching bot detection and captcha configs - test/tools/auth0/handlers/attackProtection.tests.js: add test to skip updates when attackProtection is null - test/tools/auth0/handlers/attackProtection.tests.js: add test to skip updates when attackProtection is an empty object - test/tools/auth0/handlers/attackProtection.tests.js: add test to skip botDetection update when empty object - test/tools/auth0/handlers/attackProtection.tests.js: add test to skip captcha update when empty object - test/tools/auth0/handlers/attackProtection.tests.js: add test to clean up empty captcha providers before update - test/tools/auth0/handlers/attackProtection.tests.js: add test to skip captcha update when updateCaptchaConfig is not a function - test/tools/auth0/handlers/attackProtection.tests.js: add test to return cached existing data on subsequent calls * feat: update attack protection handling - src/context/yaml/handlers/attackProtection.ts: fix maskedAttackProtection assignment to use attackProtectionConfig - src/tools/auth0/handlers/attackProtection.ts: simplify attackProtectionClient type assertion - src/tools/auth0/handlers/attackProtection.ts: enhance site_key check for captcha provider configuration * feat: simplify bot detection and captcha configuration updates - src/tools/auth0/handlers/attackProtection.ts: streamline bot detection config update logic - src/tools/auth0/handlers/attackProtection.ts: remove unnecessary checks for captcha config updates - src/tools/auth0/handlers/attackProtection.ts: optimize captcha provider handling * feat: clean up CAPTCHA provider configurations and update tests - src/tools/auth0/handlers/attackProtection.ts: remove empty CAPTCHA provider configurations before updates to prevent API errors - test/tools/auth0/handlers/attackProtection.tests.js: remove redundant test for skipping captcha update when updateCaptchaConfig is not a function
1 parent 307224d commit f0010e7

14 files changed

Lines changed: 948 additions & 51 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"bot_detection_level": "medium",
3+
"challenge_password_policy": "when_risky",
4+
"challenge_passwordless_policy": "when_risky",
5+
"challenge_password_reset_policy": "when_risky",
6+
"allowlist": [
7+
"192.168.1.0/24",
8+
"10.0.0.1"
9+
],
10+
"monitoring_mode_enabled": false
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"enabled": true,
3+
"shields": [
4+
"block",
5+
"admin_notification"
6+
],
7+
"admin_notification_frequency": [
8+
"immediately"
9+
],
10+
"method": "standard"
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"enabled": true,
3+
"shields": [
4+
"block",
5+
"user_notification"
6+
],
7+
"mode": "count_per_identifier_and_ip",
8+
"allowlist": [],
9+
"max_attempts": 10
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"active_provider_id": "friendly_captcha",
3+
"friendly_captcha": {
4+
"site_key": "##CAPTCHA_FRIENDLY_CAPTCHA_SITE_KEY##",
5+
"secret": "##CAPTCHA_FRIENDLY_CAPTCHA_SECRET##"
6+
},
7+
"recaptcha_v2": {
8+
"site_key": "##CAPTCHA_RECAPTCHA_V2_SITE_KEY##",
9+
"secret": "##CAPTCHA_RECAPTCHA_V2_SECRET##"
10+
},
11+
"recaptcha_enterprise": {
12+
"site_key": "##CAPTCHA_RECAPTCHA_ENTERPRISE_SITE_KEY##",
13+
"api_key": "##CAPTCHA_RECAPTCHA_ENTERPRISE_API_KEY##",
14+
"project_id": "##CAPTCHA_RECAPTCHA_ENTERPRISE_PROJECT_ID##"
15+
},
16+
"hcaptcha": {
17+
"site_key": "##CAPTCHA_HCAPTCHA_SITE_KEY##",
18+
"secret": "##CAPTCHA_HCAPTCHA_SECRET##"
19+
},
20+
"arkose": {
21+
"site_key": "##CAPTCHA_ARKOSE_SITE_KEY##",
22+
"secret": "##CAPTCHA_ARKOSE_SECRET##",
23+
"client_subdomain": "my-client-subdomain",
24+
"verify_subdomain": "my-verify-subdomain",
25+
"fail_open": false
26+
}
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"enabled": true,
3+
"shields": [
4+
"block",
5+
"admin_notification"
6+
],
7+
"allowlist": [
8+
"127.0.0.1"
9+
],
10+
"stage": {
11+
"pre-login": {
12+
"max_attempts": 100,
13+
"rate": 864000
14+
},
15+
"pre-user-registration": {
16+
"max_attempts": 50,
17+
"rate": 1200
18+
}
19+
}
20+
}

examples/yaml/tenant.yaml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,65 @@ customDomains:
310310
domain_metadata:
311311
myKey: value
312312

313+
attackProtection:
314+
botDetection:
315+
bot_detection_level: medium
316+
challenge_password_policy: when_risky
317+
challenge_passwordless_policy: when_risky
318+
challenge_password_reset_policy: when_risky
319+
allowlist:
320+
- 192.168.1.0/24
321+
- 10.0.0.1
322+
monitoring_mode_enabled: false
323+
captcha:
324+
active_provider_id: friendly_captcha
325+
friendly_captcha:
326+
site_key: ##CAPTCHA_FRIENDLY_CAPTCHA_SITE_KEY##
327+
secret: ##CAPTCHA_FRIENDLY_CAPTCHA_SECRET##
328+
recaptcha_v2:
329+
site_key: ##CAPTCHA_RECAPTCHA_V2_SITE_KEY##
330+
secret: ##CAPTCHA_RECAPTCHA_V2_SECRET##
331+
recaptcha_enterprise:
332+
site_key: ##CAPTCHA_RECAPTCHA_ENTERPRISE_SITE_KEY##
333+
api_key: ##CAPTCHA_RECAPTCHA_ENTERPRISE_API_KEY##
334+
project_id: ##CAPTCHA_RECAPTCHA_ENTERPRISE_PROJECT_ID##
335+
hcaptcha:
336+
site_key: ##CAPTCHA_HCAPTCHA_SITE_KEY##
337+
secret: ##CAPTCHA_HCAPTCHA_SECRET##
338+
arkose:
339+
site_key: ##CAPTCHA_ARKOSE_SITE_KEY##
340+
secret: ##CAPTCHA_ARKOSE_SECRET##
341+
client_subdomain: my-client-subdomain
342+
verify_subdomain: my-verify-subdomain
343+
fail_open: false
344+
breachedPasswordDetection:
345+
enabled: true
346+
shields:
347+
- block
348+
- admin_notification
349+
admin_notification_frequency:
350+
- immediately
351+
method: standard
352+
bruteForceProtection:
353+
enabled: true
354+
shields:
355+
- block
356+
- user_notification
357+
mode: count_per_identifier_and_ip
358+
allowlist: []
359+
max_attempts: 10
360+
suspiciousIpThrottling:
361+
enabled: true
362+
shields:
363+
- block
364+
- admin_notification
365+
allowlist:
366+
- 127.0.0.1
367+
stage:
368+
pre-login:
369+
max_attempts: 100
370+
rate: 864000
371+
pre-user-registration:
372+
max_attempts: 50
373+
rate: 1200
374+

src/context/defaults.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AttackProtection } from '../tools/auth0/handlers/attackProtection';
12
import { maskSecretAtPath } from '../tools/utils';
23

34
// eslint-disable-next-line import/prefer-default-export
@@ -129,3 +130,32 @@ export function logStreamDefaults(logStreams) {
129130

130131
return maskedLogStreams;
131132
}
133+
134+
export function attackProtectionDefaults(attackProtection: AttackProtection) {
135+
const { captcha } = attackProtection;
136+
137+
if (captcha) {
138+
const providersWithSecrets = ['arkose', 'hcaptcha', 'friendly_captcha', 'recaptcha_v2'];
139+
140+
providersWithSecrets.forEach((provider) => {
141+
if (captcha[provider]) {
142+
captcha[provider] = {
143+
...captcha[provider],
144+
secret: `##CAPTCHA_${provider.toUpperCase()}_SECRET##`,
145+
};
146+
}
147+
});
148+
149+
if ('recaptcha_enterprise' in captcha) {
150+
captcha.recaptcha_enterprise = {
151+
...captcha.recaptcha_enterprise,
152+
api_key: '##CAPTCHA_RECAPTCHA_ENTERPRISE_API_KEY##',
153+
project_id: '##CAPTCHA_RECAPTCHA_ENTERPRISE_PROJECT_ID##',
154+
};
155+
}
156+
157+
attackProtection.captcha = captcha;
158+
}
159+
160+
return attackProtection;
161+
}

src/context/directory/handlers/attackProtection.ts

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,28 @@ import { constants } from '../../../tools';
44
import { dumpJSON, existsMustBeDir, loadJSON } from '../../../utils';
55
import { DirectoryHandler } from '.';
66
import DirectoryContext from '..';
7-
import { Asset, ParsedAsset } from '../../../types';
7+
import { ParsedAsset } from '../../../types';
8+
import { attackProtectionDefaults } from '../../defaults';
9+
import { AttackProtection } from '../../../tools/auth0/handlers/attackProtection';
810

9-
type ParsedAttackProtection = ParsedAsset<
10-
'attackProtection',
11-
{
12-
breachedPasswordDetection: Asset;
13-
bruteForceProtection: Asset;
14-
suspiciousIpThrottling: Asset;
15-
}
16-
>;
11+
type ParsedAttackProtection = ParsedAsset<'attackProtection', AttackProtection>;
1712

1813
function attackProtectionFiles(filePath: string): {
1914
directory: string;
15+
botDetection: string;
2016
breachedPasswordDetection: string;
2117
bruteForceProtection: string;
18+
captcha: string;
2219
suspiciousIpThrottling: string;
2320
} {
2421
const directory = path.join(filePath, constants.ATTACK_PROTECTION_DIRECTORY);
2522

2623
return {
2724
directory: directory,
25+
botDetection: path.join(directory, 'bot-detection.json'),
2826
breachedPasswordDetection: path.join(directory, 'breached-password-detection.json'),
2927
bruteForceProtection: path.join(directory, 'brute-force-protection.json'),
28+
captcha: path.join(directory, 'captcha.json'),
3029
suspiciousIpThrottling: path.join(directory, 'suspicious-ip-throttling.json'),
3130
};
3231
}
@@ -40,6 +39,10 @@ function parse(context: DirectoryContext): ParsedAttackProtection {
4039
};
4140
}
4241

42+
const botDetection = loadJSON(files.botDetection, {
43+
mappings: context.mappings,
44+
disableKeywordReplacement: context.disableKeywordReplacement,
45+
});
4346
const breachedPasswordDetection = loadJSON(files.breachedPasswordDetection, {
4447
mappings: context.mappings,
4548
disableKeywordReplacement: context.disableKeywordReplacement,
@@ -48,17 +51,25 @@ function parse(context: DirectoryContext): ParsedAttackProtection {
4851
mappings: context.mappings,
4952
disableKeywordReplacement: context.disableKeywordReplacement,
5053
});
54+
const captcha = loadJSON(files.captcha, {
55+
mappings: context.mappings,
56+
disableKeywordReplacement: context.disableKeywordReplacement,
57+
});
5158
const suspiciousIpThrottling = loadJSON(files.suspiciousIpThrottling, {
5259
mappings: context.mappings,
5360
disableKeywordReplacement: context.disableKeywordReplacement,
5461
});
5562

63+
const maskedAttackProtection = attackProtectionDefaults({
64+
botDetection,
65+
breachedPasswordDetection,
66+
bruteForceProtection,
67+
captcha,
68+
suspiciousIpThrottling,
69+
});
70+
5671
return {
57-
attackProtection: {
58-
breachedPasswordDetection,
59-
bruteForceProtection,
60-
suspiciousIpThrottling,
61-
},
72+
attackProtection: maskedAttackProtection,
6273
};
6374
}
6475

@@ -70,9 +81,21 @@ async function dump(context: DirectoryContext): Promise<void> {
7081
const files = attackProtectionFiles(context.filePath);
7182
fs.ensureDirSync(files.directory);
7283

73-
dumpJSON(files.breachedPasswordDetection, attackProtection.breachedPasswordDetection);
74-
dumpJSON(files.bruteForceProtection, attackProtection.bruteForceProtection);
75-
dumpJSON(files.suspiciousIpThrottling, attackProtection.suspiciousIpThrottling);
84+
if (attackProtection.botDetection) {
85+
dumpJSON(files.botDetection, attackProtection.botDetection);
86+
}
87+
if (attackProtection.breachedPasswordDetection) {
88+
dumpJSON(files.breachedPasswordDetection, attackProtection.breachedPasswordDetection);
89+
}
90+
if (attackProtection.bruteForceProtection) {
91+
dumpJSON(files.bruteForceProtection, attackProtection.bruteForceProtection);
92+
}
93+
if (attackProtection.captcha) {
94+
dumpJSON(files.captcha, attackProtection.captcha);
95+
}
96+
if (attackProtection.suspiciousIpThrottling) {
97+
dumpJSON(files.suspiciousIpThrottling, attackProtection.suspiciousIpThrottling);
98+
}
7699
}
77100

78101
const attackProtectionHandler: DirectoryHandler<ParsedAttackProtection> = {

src/context/yaml/handlers/attackProtection.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,42 @@
11
import { YAMLHandler } from '.';
22
import YAMLContext from '..';
3-
import { Asset, ParsedAsset } from '../../../types';
4-
5-
type ParsedAttackProtection = ParsedAsset<
6-
'attackProtection',
7-
{
8-
breachedPasswordDetection: Asset;
9-
bruteForceProtection: Asset;
10-
suspiciousIpThrottling: Asset;
11-
}
12-
>;
3+
import { AttackProtection } from '../../../tools/auth0/handlers/attackProtection';
4+
import { ParsedAsset } from '../../../types';
5+
import { attackProtectionDefaults } from '../../defaults';
6+
7+
type ParsedAttackProtection = ParsedAsset<'attackProtection', AttackProtection>;
138

149
async function parseAndDump(context: YAMLContext): Promise<ParsedAttackProtection> {
1510
const { attackProtection } = context.assets;
1611

1712
if (!attackProtection) return { attackProtection: null };
1813

19-
const { suspiciousIpThrottling, breachedPasswordDetection, bruteForceProtection } =
20-
attackProtection;
14+
const {
15+
botDetection,
16+
suspiciousIpThrottling,
17+
breachedPasswordDetection,
18+
bruteForceProtection,
19+
captcha,
20+
} = attackProtection;
21+
22+
const attackProtectionConfig: ParsedAttackProtection['attackProtection'] = {
23+
suspiciousIpThrottling,
24+
breachedPasswordDetection,
25+
bruteForceProtection,
26+
};
27+
28+
if (botDetection) {
29+
attackProtectionConfig.botDetection = botDetection;
30+
}
31+
32+
if (captcha) {
33+
attackProtectionConfig.captcha = captcha;
34+
}
35+
36+
const maskedAttackProtection = attackProtectionDefaults(attackProtectionConfig);
2137

2238
return {
23-
attackProtection: {
24-
suspiciousIpThrottling,
25-
breachedPasswordDetection,
26-
bruteForceProtection,
27-
},
39+
attackProtection: maskedAttackProtection,
2840
};
2941
}
3042

0 commit comments

Comments
 (0)