Skip to content

Commit ad4b3e7

Browse files
feat: captcha
* Added Revis distributed cash to enhance our Captcha Verification system so that we prevent our system from replay attacks * Fix: There was an error with the implementation of Redis, so I reverted to our previous version that uses in memory storage * Integrated the captcha verification system into our sign in Form. The captcha verification system now works on both login and sign int * Remove test files from captcha module * Update src/backend/src/modules/captcha/middleware/captcha-middleware.js Co-authored-by: Eric Dubé <[email protected]> * Update src/backend/src/modules/captcha/middleware/captcha-middleware.js Co-authored-by: Eric Dubé <[email protected]> * Now the captcha can be requested on condition, this llaows extenstions to control wether a captcha should be required, I fixed the code in CaptchaModule to use config and got rid of the lines that made captcha middleware available since it wasn't used anywhre * I split the middleware into two distinct parts, so that the frontend can now determine captach requirements. PuterHomePageService can set GUI parameters for captcha requirements. The /whoarewe endpoint provides captcha requirement information and the extensuo system integration is maintained * Fix security issues with password handling in URL query parameters * Made sure that the enter key, submits the login request instead of refreshing the captcha * In development we can now disable the Captcha verification system by running it with CAPTCHA_ENABLED=false npm start * Went back and modified checkCaptcha so that it checks at the start to check what CAPTCHA_ENABLED is equal to * Refactor captcha system to use configuration values instead of environment variables * Fix captcha verification and align with project standards * Update src/backend/src/modules/captcha/README.md Co-authored-by: Eric Dubé <[email protected]> * fix: incorrect service name * dev: use Endpoint for captcha endpoints Use Endpoint class, which uses eggspress behind the scenes, which handles async errors in handlers automatically. * dev: add extension support and simplify captcha - removed extra error handling - removed dormant code - no distinction between login and signup (for now) * clean: remove local files * fix: undefined edge case --------- Co-authored-by: Eric Dubé <[email protected]>
1 parent f73958e commit ad4b3e7

30 files changed

+11167
-7139
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ dist/
2929

3030
# Local Netlify folder
3131
.netlify
32-
src/emulator/release/
32+
src/emulator/release/

package-lock.json

Lines changed: 5297 additions & 7082 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"dependencies": {
5050
"@heyputer/putility": "^1.0.2",
5151
"dedent": "^1.5.3",
52+
"ioredis": "^5.6.0",
5253
"javascript-time-ago": "^2.5.11",
5354
"json-colorizer": "^3.0.1",
5455
"open": "^10.1.0",

src/backend/exports.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const { InternetModule } = require("./src/modules/internet/InternetModule.js");
3939
const { PuterExecModule } = require("./src/modules/puterexec/PuterExecModule.js");
4040
const { MailModule } = require("./src/modules/mail/MailModule.js");
4141
const { ConvertModule } = require("./src/modules/convert/ConvertModule.js");
42+
const { CaptchaModule } = require("./src/modules/captcha/CaptchaModule.js");
4243

4344
module.exports = {
4445
helloworld: () => {
@@ -61,6 +62,7 @@ module.exports = {
6162
WebModule,
6263
TemplateModule,
6364
AppsModule,
65+
CaptchaModule,
6466
],
6567

6668
// Pre-built modules
@@ -76,6 +78,7 @@ module.exports = {
7678
InternetModule,
7779
MailModule,
7880
ConvertModule,
81+
CaptchaModule,
7982

8083
// Development modules
8184
PerfMonModule,

src/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@smithy/node-http-handler": "^2.2.2",
2525
"args": "^5.0.3",
2626
"aws-sdk": "^2.1383.0",
27-
"axios": "^1.4.0",
27+
"axios": "^1.8.2",
2828
"bcrypt": "^5.1.0",
2929
"better-sqlite3": "^11.9.0",
3030
"busboy": "^1.6.0",
@@ -73,6 +73,7 @@
7373
"ssh2": "^1.13.0",
7474
"string-hash": "^1.1.3",
7575
"string-length": "^6.0.0",
76+
"svg-captcha": "^1.4.0",
7677
"svgo": "^3.0.2",
7778
"tiktoken": "^1.0.16",
7879
"together-ai": "^0.6.0-alpha.4",

src/backend/src/api/APIError.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,17 @@ module.exports = class APIError {
483483
'not_yet_supported': {
484484
status: 400,
485485
message: ({ message }) => message,
486-
}
486+
},
487+
488+
// Captcha errors
489+
'captcha_required': {
490+
status: 400,
491+
message: ({ message }) => message || 'Captcha verification required',
492+
},
493+
'captcha_invalid': {
494+
status: 400,
495+
message: ({ message }) => message || 'Invalid captcha response',
496+
},
487497
};
488498

489499
/**

src/backend/src/config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ config.require_email_verification_to_publish_website = false;
5151
config.kv_max_key_size = 1024;
5252
config.kv_max_value_size = 400 * 1024;
5353

54+
// Captcha configuration
55+
config.captcha = {
56+
enabled: false, // Enable captcha by default
57+
expirationTime: 10 * 60 * 1000, // 10 minutes default expiration time
58+
difficulty: 'medium' // Default difficulty level
59+
};
60+
5461
config.monitor = {
5562
metricsInterval: 60000,
5663
windowSize: 30,

src/backend/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020

2121
const { Kernel } = require("./Kernel");
2222
const CoreModule = require("./CoreModule");
23+
const { CaptchaModule } = require("./modules/captcha/CaptchaModule"); // Add CaptchaModule
2324

2425
const testlaunch = () => {
2526
const k = new Kernel();
2627
k.add_module(new CoreModule());
28+
k.add_module(new CaptchaModule()); // Register the CaptchaModule
2729
k.boot();
2830
}
2931

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// METADATA // {"ai-commented":{"service":"claude"}}
2+
/*
3+
* Copyright (C) 2024-present Puter Technologies Inc.
4+
*
5+
* This file is part of Puter.
6+
*
7+
* Puter is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as published
9+
* by the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
const { AdvancedBase } = require("@heyputer/putility");
22+
const CaptchaService = require('./services/CaptchaService');
23+
24+
/**
25+
* @class CaptchaModule
26+
* @extends AdvancedBase
27+
* @description Module that provides captcha verification functionality to protect
28+
* against automated abuse, particularly for login and signup flows. Registers
29+
* a CaptchaService for generating and verifying captchas as well as middlewares
30+
* that can be used to protect routes and determine captcha requirements.
31+
*/
32+
class CaptchaModule extends AdvancedBase {
33+
async install(context) {
34+
console.log('DIAGNOSTIC: CaptchaModule.install - Start of method');
35+
36+
// Get services from context
37+
const services = context.get('services');
38+
if (!services) {
39+
throw new Error('Services not available in context');
40+
}
41+
42+
// Register the captcha service
43+
console.log('DIAGNOSTIC: CaptchaModule.install - Before service registration');
44+
services.registerService('captcha', CaptchaService);
45+
console.log('DIAGNOSTIC: CaptchaModule.install - After service registration');
46+
47+
// Log the captcha service status
48+
try {
49+
const captchaService = services.get('captcha');
50+
console.log(`Captcha service registered and ${captchaService.enabled ? 'enabled' : 'disabled'}`);
51+
console.log('TOKENS_TRACKING: Retrieved CaptchaService instance with ID:', captchaService.serviceId);
52+
} catch (error) {
53+
console.error('Failed to get captcha service after registration:', error);
54+
}
55+
}
56+
}
57+
58+
module.exports = { CaptchaModule };
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Captcha Module
2+
3+
This module provides captcha verification functionality to protect against automated abuse, particularly for login and signup flows.
4+
5+
## Components
6+
7+
- **CaptchaModule.js**: Registers the service and middleware
8+
- **CaptchaService.js**: Provides captcha generation and verification functionality
9+
- **captcha-middleware.js**: Express middleware for protecting routes with captcha verification
10+
11+
## Integration
12+
13+
The CaptchaService is registered by the CaptchaModule and can be accessed by other services:
14+
15+
```javascript
16+
const captchaService = services.get('captcha');
17+
```
18+
19+
### Example Usage
20+
21+
```javascript
22+
// Generate a captcha
23+
const captcha = captchaService.generateCaptcha();
24+
// captcha.token - The token to verify later
25+
// captcha.image - SVG image data to display to the user
26+
27+
// Verify a captcha
28+
const isValid = captchaService.verifyCaptcha(token, userAnswer);
29+
```
30+
31+
## Configuration
32+
33+
The CaptchaService can be configured with the following options in the configuration file (`config.json`):
34+
35+
- `captcha.enabled`: Whether the captcha service is enabled (default: false)
36+
- `captcha.expirationTime`: How long captcha tokens are valid in milliseconds (default: 10 minutes)
37+
- `captcha.difficulty`: The difficulty level of the captcha ('easy', 'medium', 'hard') (default: 'medium')
38+
39+
These options are set in the main configuration file. For example:
40+
41+
```json
42+
{
43+
"services": {
44+
"captcha": {
45+
"enabled": false,
46+
"expirationTime": 600000,
47+
"difficulty": "medium"
48+
}
49+
}
50+
}
51+
```
52+
53+
### Development Configuration
54+
55+
For local development, you can disable captcha by creating or modifying your local configuration file (e.g., in `volatile/config/config.json` or using a profile configuration):
56+
57+
```json
58+
{
59+
"$version": "v1.1.0",
60+
"$requires": [
61+
"config.json"
62+
],
63+
"config_name": "local",
64+
65+
"services": {
66+
"captcha": {
67+
"enabled": false
68+
}
69+
}
70+
}
71+
```
72+
73+
These options are set when registering the service in CaptchaModule.js.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Captcha Middleware
2+
3+
This middleware provides captcha verification for routes that need protection against automated abuse.
4+
5+
## Middleware Components
6+
7+
The captcha system is now split into two middleware components:
8+
9+
1. **checkCaptcha**: Determines if captcha verification is required but doesn't perform verification.
10+
2. **requireCaptcha**: Performs actual captcha verification based on the result from checkCaptcha.
11+
12+
This split allows frontend applications to know in advance whether captcha verification will be needed for a particular action.
13+
14+
## Usage Patterns
15+
16+
### Using Both Middlewares (Recommended)
17+
18+
For best user experience, use both middlewares together:
19+
20+
```javascript
21+
const express = require('express');
22+
const router = express.Router();
23+
24+
// Get both middleware components from the context
25+
const { checkCaptcha, requireCaptcha } = context.get('captcha-middleware');
26+
27+
// Determine if captcha is required for this route
28+
router.post('/login', checkCaptcha({ eventType: 'login' }), (req, res, next) => {
29+
// Set a flag in the response so frontend knows if captcha is needed
30+
res.locals.captchaRequired = req.captchaRequired;
31+
next();
32+
}, requireCaptcha(), (req, res) => {
33+
// Handle login logic
34+
// If captcha was required, it has been verified at this point
35+
});
36+
```
37+
38+
### Using Individual Middlewares
39+
40+
You can also access each middleware separately:
41+
42+
```javascript
43+
const checkCaptcha = context.get('check-captcha-middleware');
44+
const requireCaptcha = context.get('require-captcha-middleware');
45+
```
46+
47+
### Using Only requireCaptcha (Legacy Mode)
48+
49+
For backward compatibility, you can still use only the requireCaptcha middleware:
50+
51+
```javascript
52+
const requireCaptcha = context.get('require-captcha-middleware');
53+
54+
// Always require captcha for this route
55+
router.post('/sensitive-route', requireCaptcha({ always: true }), (req, res) => {
56+
// Route handler
57+
});
58+
59+
// Conditionally require captcha based on extensions
60+
router.post('/normal-route', requireCaptcha(), (req, res) => {
61+
// Route handler
62+
});
63+
```
64+
65+
## Configuration Options
66+
67+
### checkCaptcha Options
68+
69+
- `always` (boolean): Always require captcha regardless of other factors
70+
- `strictMode` (boolean): If true, fails closed on errors (more secure)
71+
- `eventType` (string): Type of event for extensions (e.g., 'login', 'signup')
72+
73+
### requireCaptcha Options
74+
75+
- `strictMode` (boolean): If true, fails closed on errors (more secure)
76+
77+
## Frontend Integration
78+
79+
There are two ways to integrate with the frontend:
80+
81+
### 1. Using the checkCaptcha Result in API Responses
82+
83+
You can include the captcha requirement in API responses:
84+
85+
```javascript
86+
router.get('/whoarewe', checkCaptcha({ eventType: 'login' }), (req, res) => {
87+
res.json({
88+
// Other environment information
89+
captchaRequired: {
90+
login: req.captchaRequired
91+
}
92+
});
93+
});
94+
```
95+
96+
### 2. Setting GUI Parameters
97+
98+
For PuterHomepageService, you can add captcha requirements to GUI parameters:
99+
100+
```javascript
101+
// In PuterHomepageService.js
102+
gui_params: {
103+
// Other parameters
104+
captchaRequired: {
105+
login: req.captchaRequired
106+
}
107+
}
108+
```
109+
110+
## Client-Side Integration
111+
112+
To integrate with the captcha middleware, the client needs to:
113+
114+
1. Check if captcha is required for the action (using /whoarewe or GUI parameters)
115+
2. If required, call the `/api/captcha/generate` endpoint to get a captcha token and image
116+
3. Display the captcha image to the user and collect their answer
117+
4. Include the captcha token and answer in the request body:
118+
119+
```javascript
120+
// Example client-side code
121+
async function submitWithCaptcha(formData) {
122+
// Check if captcha is required
123+
const envInfo = await fetch('/api/whoarewe').then(r => r.json());
124+
125+
if (envInfo.captchaRequired?.login) {
126+
// Get and display captcha to user
127+
const captcha = await getCaptchaFromServer();
128+
showCaptchaToUser(captcha);
129+
130+
// Add captcha token and answer to the form data
131+
formData.captchaToken = captcha.token;
132+
formData.captchaAnswer = await getUserCaptchaAnswer();
133+
}
134+
135+
// Submit the form
136+
const response = await fetch('/api/login', {
137+
method: 'POST',
138+
headers: {
139+
'Content-Type': 'application/json'
140+
},
141+
body: JSON.stringify(formData)
142+
});
143+
144+
// Handle response
145+
const data = await response.json();
146+
if (response.status === 400 && data.error === 'captcha_required') {
147+
// Show captcha to the user if not already shown
148+
showCaptcha();
149+
}
150+
}
151+
```
152+
153+
## Error Handling
154+
155+
The middleware will throw the following errors:
156+
157+
- `captcha_required`: When captcha verification is required but no token or answer was provided.
158+
- `captcha_invalid`: When the provided captcha answer is incorrect.
159+
160+
These errors can be caught by the API error handler and returned to the client.

0 commit comments

Comments
 (0)