Skip to content

Commit 0cc186c

Browse files
authored
Merge pull request #23 from HarperFast/debug-limit
Limit debug endpoint access by allowlist controlled IPs when debug is enabled
2 parents b69b978 + afb5389 commit 0cc186c

File tree

5 files changed

+385
-4
lines changed

5 files changed

+385
-4
lines changed

docs/configuration.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,44 @@ When `debug: true` is enabled, additional endpoints are available:
300300
- `GET /oauth/{provider}/user` - Current user info and token status
301301
- `GET /oauth/{provider}/refresh` - Trigger token refresh
302302

303-
**Warning:** Never enable debug mode in production environments.
303+
### Security: IP-Based Access Control
304+
305+
**By default, debug endpoints are only accessible from localhost** (`127.0.0.1` and `::1`). This prevents unauthorized access to sensitive debugging information.
306+
307+
To allow access from other IPs, set the `DEBUG_ALLOWED_IPS` environment variable:
308+
309+
```bash
310+
# Allow single IP
311+
DEBUG_ALLOWED_IPS=192.168.1.100
312+
313+
# Allow multiple IPs (comma-separated)
314+
DEBUG_ALLOWED_IPS=192.168.1.100,192.168.1.101,10.0.0.50
315+
316+
# Allow IP range using prefix matching (e.g., all 10.0.0.x)
317+
DEBUG_ALLOWED_IPS=10.0.0.
318+
319+
# Deny all access (empty string)
320+
DEBUG_ALLOWED_IPS=
321+
```
322+
323+
**Access denial response:**
324+
325+
```json
326+
{
327+
"error": "Access forbidden",
328+
"message": "Debug endpoints are only accessible from allowed IPs.",
329+
"hint": "Set DEBUG_ALLOWED_IPS environment variable to allow access from your IP. Defaults to localhost only (127.0.0.1,::1)."
330+
}
331+
```
332+
333+
**Security best practices:**
334+
335+
- Keep debug mode disabled in production
336+
- Use IP allowlist when debug mode must be enabled remotely
337+
- Monitor access logs for unauthorized attempts
338+
- Regular endpoints (`/login`, `/callback`, `/logout`) are not affected by IP restrictions
339+
340+
**Warning:** Never enable debug mode in production environments without strict IP controls.
304341

305342
## Complete Example
306343

src/lib/resource.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,65 @@ export class OAuthResource extends Resource {
9393
return false;
9494
}
9595

96+
/**
97+
* Check if a request is allowed to access debug endpoints
98+
* Uses IP allowlist for security (defaults to localhost only)
99+
*
100+
* @param request - The incoming request
101+
* @param logger - Optional logger for access tracking
102+
* @returns true if access is allowed, false otherwise
103+
*/
104+
static checkDebugAccess(request: Request, logger?: Logger): boolean {
105+
// Get IP allowlist from environment variable or use localhost-only default
106+
// Use ?? to allow empty string (which denies all access)
107+
const DEBUG_ALLOWED_IPS = process.env.DEBUG_ALLOWED_IPS ?? '127.0.0.1,::1';
108+
const allowedIps = DEBUG_ALLOWED_IPS.split(',').map((ip) => ip.trim());
109+
const clientIp = request.ip || '';
110+
111+
// Check if client IP matches any allowed IP
112+
let ipAllowed = false;
113+
for (const allowed of allowedIps) {
114+
// Exact match
115+
if (allowed === clientIp) {
116+
ipAllowed = true;
117+
break;
118+
}
119+
// Simple prefix match for CIDR-like patterns (e.g., "10.0.0." matches "10.0.0.1")
120+
if (allowed.endsWith('.') && clientIp.startsWith(allowed)) {
121+
ipAllowed = true;
122+
break;
123+
}
124+
}
125+
126+
// Log access attempt
127+
if (ipAllowed) {
128+
logger?.info?.('OAuth debug endpoint accessed', {
129+
ip: clientIp,
130+
});
131+
} else {
132+
logger?.warn?.('OAuth debug endpoint access denied - unauthorized IP', {
133+
ip: clientIp,
134+
allowedIps,
135+
});
136+
}
137+
138+
return ipAllowed;
139+
}
140+
141+
/**
142+
* Build forbidden response for unauthorized debug access
143+
*/
144+
static forbiddenResponse(): any {
145+
return {
146+
status: 403,
147+
body: {
148+
error: 'Access forbidden',
149+
message: 'Debug endpoints are only accessible from allowed IPs.',
150+
hint: 'Set DEBUG_ALLOWED_IPS environment variable to allow access from your IP. Defaults to localhost only (127.0.0.1,::1).',
151+
},
152+
};
153+
}
154+
96155
/**
97156
* Build the standard 404 response
98157
*/
@@ -209,6 +268,13 @@ export class OAuthResource extends Resource {
209268
return OAuthResource.notFoundResponse();
210269
}
211270

271+
// If debug mode is enabled and this is a debug-only route, check IP allowlist
272+
if (debugMode && OAuthResource.isDebugOnlyRoute(route)) {
273+
if (!OAuthResource.checkDebugAccess(request, logger)) {
274+
return OAuthResource.forbiddenResponse();
275+
}
276+
}
277+
212278
// Special case: /oauth/test without provider
213279
if (providerName === 'test' && !action) {
214280
return handleTestPage(logger);

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ export interface Request extends IncomingMessage {
261261
user?: User | string;
262262
session?: Session;
263263
headers: IncomingMessage['headers'];
264+
/** Client IP address */
265+
ip?: string;
264266
}
265267

266268
/**

test/lib/OAuthResource.responses.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import { describe, it, afterEach } from 'node:test';
77
import assert from 'node:assert/strict';
88

9-
// Note: Bun uses test/bun-preload.js to mock the harperdb module
10-
// Node.js will load the real harperdb module, which may trigger async
11-
// native module loading that continues after tests complete. This is harmless.
9+
// Mock Harper's Resource class
10+
global.Resource = class {
11+
static loadAsInstance = false;
12+
};
1213

1314
import { OAuthResource } from '../../dist/lib/resource.js';
1415

0 commit comments

Comments
 (0)