@l4yercak3/cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l4yercak3/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Icing on the L4yercak3 - The sweet finishing touch for your Layer Cake integration",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -3,6 +3,7 @@
3
3
  * Handles communication with L4YERCAK3 backend API
4
4
  */
5
5
 
6
+ const crypto = require('crypto');
6
7
  const fetch = require('node-fetch');
7
8
  const configManager = require('../config/config-manager');
8
9
 
@@ -11,6 +12,13 @@ class BackendClient {
11
12
  this.baseUrl = configManager.getBackendUrl();
12
13
  }
13
14
 
15
+ /**
16
+ * Generate a cryptographically secure state token for CSRF protection
17
+ */
18
+ generateState() {
19
+ return crypto.randomBytes(32).toString('hex');
20
+ }
21
+
14
22
  /**
15
23
  * Get headers for API requests
16
24
  */
@@ -98,18 +106,21 @@ class BackendClient {
98
106
  }
99
107
 
100
108
  /**
101
- * Get CLI login URL (uses unified OAuth signup endpoint)
109
+ * Get CLI login URL with state parameter for CSRF protection
110
+ * @param {string} state - The state token generated by the CLI
111
+ * @param {string|null} provider - Optional OAuth provider for direct auth
112
+ * @returns {string} The login URL
102
113
  */
103
- getLoginUrl(provider = null) {
114
+ getLoginUrl(state, provider = null) {
104
115
  const backendUrl = configManager.getBackendUrl();
105
116
  const callbackUrl = 'http://localhost:3000/callback';
106
-
117
+
107
118
  if (provider) {
108
119
  // Direct OAuth provider URL
109
- return `${backendUrl}/api/auth/oauth-signup?provider=${provider}&sessionType=cli&callback=${encodeURIComponent(callbackUrl)}`;
120
+ return `${backendUrl}/api/auth/oauth-signup?provider=${provider}&sessionType=cli&state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
110
121
  } else {
111
- // Provider selection page (still uses old endpoint for now, but could be updated)
112
- return `${backendUrl}/auth/cli-login?callback=${encodeURIComponent(callbackUrl)}`;
122
+ // Provider selection page
123
+ return `${backendUrl}/auth/cli-login?state=${state}&callback=${encodeURIComponent(callbackUrl)}`;
113
124
  }
114
125
  }
115
126
 
@@ -10,17 +10,37 @@ const chalk = require('chalk');
10
10
 
11
11
  /**
12
12
  * Start local server to receive OAuth callback
13
+ * @param {string} expectedState - The state token to verify against
13
14
  */
14
- function startCallbackServer() {
15
+ function startCallbackServer(expectedState) {
15
16
  return new Promise((resolve, reject) => {
16
17
  const http = require('http');
17
-
18
+
18
19
  const server = http.createServer((req, res) => {
19
20
  const url = new URL(req.url, 'http://localhost:3000');
20
-
21
+
21
22
  if (url.pathname === '/callback') {
22
23
  const token = url.searchParams.get('token');
23
-
24
+ const returnedState = url.searchParams.get('state');
25
+
26
+ // Verify state to prevent CSRF attacks
27
+ if (returnedState !== expectedState) {
28
+ res.writeHead(400, { 'Content-Type': 'text/html' });
29
+ res.end(`
30
+ <html>
31
+ <head><title>CLI Login Error</title></head>
32
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
33
+ <h1 style="color: #EF4444;">❌ Security Error</h1>
34
+ <p>State mismatch - possible CSRF attack. Please try again.</p>
35
+ </body>
36
+ </html>
37
+ `);
38
+
39
+ server.close();
40
+ reject(new Error('State mismatch - security validation failed'));
41
+ return;
42
+ }
43
+
24
44
  if (token) {
25
45
  res.writeHead(200, { 'Content-Type': 'text/html' });
26
46
  res.end(`
@@ -32,7 +52,7 @@ function startCallbackServer() {
32
52
  </body>
33
53
  </html>
34
54
  `);
35
-
55
+
36
56
  server.close();
37
57
  resolve(token);
38
58
  } else {
@@ -46,7 +66,7 @@ function startCallbackServer() {
46
66
  </body>
47
67
  </html>
48
68
  `);
49
-
69
+
50
70
  server.close();
51
71
  reject(new Error('No token received'));
52
72
  }
@@ -85,13 +105,16 @@ async function handleLogin() {
85
105
 
86
106
  console.log(chalk.cyan(' 🔐 Opening browser for authentication...\n'));
87
107
 
88
- // Start callback server
89
- const callbackPromise = startCallbackServer();
108
+ // Generate state for CSRF protection
109
+ const state = backendClient.generateState();
110
+
111
+ // Start callback server with expected state
112
+ const callbackPromise = startCallbackServer(state);
90
113
 
91
- // Open browser
92
- const loginUrl = backendClient.getLoginUrl();
114
+ // Open browser with state parameter
115
+ const loginUrl = backendClient.getLoginUrl(state);
93
116
  console.log(chalk.gray(` Login URL: ${loginUrl}\n`));
94
-
117
+
95
118
  await open(loginUrl);
96
119
 
97
120
  // Wait for callback
@@ -242,25 +242,45 @@ describe('BackendClient', () => {
242
242
  });
243
243
  });
244
244
 
245
+ describe('generateState', () => {
246
+ it('generates a 64-character hex string', () => {
247
+ const state = BackendClient.generateState();
248
+
249
+ expect(state).toMatch(/^[a-f0-9]{64}$/);
250
+ });
251
+
252
+ it('generates unique values each time', () => {
253
+ const state1 = BackendClient.generateState();
254
+ const state2 = BackendClient.generateState();
255
+
256
+ expect(state1).not.toBe(state2);
257
+ });
258
+ });
259
+
245
260
  describe('getLoginUrl', () => {
246
- it('returns provider selection URL when no provider specified', () => {
247
- const url = BackendClient.getLoginUrl();
261
+ it('returns provider selection URL with state when no provider specified', () => {
262
+ const state = 'test-state-token';
263
+ const url = BackendClient.getLoginUrl(state);
248
264
 
249
265
  expect(url).toContain('https://backend.test.com');
250
266
  expect(url).toContain('/auth/cli-login');
267
+ expect(url).toContain('state=test-state-token');
251
268
  expect(url).toContain('callback=');
252
269
  });
253
270
 
254
271
  it('returns direct OAuth URL when provider specified', () => {
255
- const url = BackendClient.getLoginUrl('google');
272
+ const state = 'test-state-token';
273
+ const url = BackendClient.getLoginUrl(state, 'google');
256
274
 
257
275
  expect(url).toContain('/api/auth/oauth-signup');
258
276
  expect(url).toContain('provider=google');
259
277
  expect(url).toContain('sessionType=cli');
278
+ expect(url).toContain('state=test-state-token');
260
279
  });
261
280
 
262
281
  it('includes encoded callback URL', () => {
263
- const url = BackendClient.getLoginUrl('github');
282
+ const state = 'test-state-token';
283
+ const url = BackendClient.getLoginUrl(state, 'github');
264
284
 
265
285
  expect(url).toContain(encodeURIComponent('http://localhost:3000/callback'));
266
286
  });