Skip to content

Conversation

vitalivu992
Copy link

@vitalivu992 vitalivu992 commented Sep 29, 2025

  • Updated user database schema to include PAM user status.
  • Enhanced user creation and retrieval methods to handle PAM users.
  • Added PAM authentication endpoints in the auth route.
  • Integrated PAM authentication logic in the frontend login form.
  • Introduced PAMAuthService for handling PAM-related operations.
  • Updated AuthContext to manage PAM login and availability status.
image

Summary by CodeRabbit

  • New Features
    • Added Linux PAM authentication option alongside existing login; users can toggle Local Account vs Linux PAM on sign-in.
    • Login UI updates labels, placeholders, button text, and contextual help based on selected mode.
    • App reports PAM availability and shows PAM option only when supported.
    • Successful PAM logins create or update user profiles and surface PAM status in the UI, supporting accounts without a local password.

- Updated user database schema to include PAM user status.
- Enhanced user creation and retrieval methods to handle PAM users.
- Added PAM authentication endpoints in the auth route.
- Integrated PAM authentication logic in the frontend login form.
- Introduced PAMAuthService for handling PAM-related operations.
- Updated AuthContext to manage PAM login and availability status.
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Adds Linux PAM authentication end-to-end: DB schema and user model include is_pam_user and nullable password_hash; backend adds a PAM auth service and routes (/pam-login, /pam-status, status includes pamAvailable); frontend exposes pamStatus/pamLogin, AuthContext state, and LoginForm PAM mode.

Changes

Cohort / File(s) Summary of changes
Database schema & model
server/database/init.sql, server/database/db.js
DB: password_hash made nullable; added is_pam_user BOOLEAN DEFAULT 0. Model: createUser(..., isPamUser = false) stores/returns is_pam_user; getUserById returns is_pam_user; new updateUserPamStatus(userId, isPamUser) and isPamUser(userId).
PAM service & auth routes
server/services/pamAuth.js, server/routes/auth.js
New PAMAuthService singleton with authenticate, authenticateWithSu, isAvailable, getUserInfo, setServiceName. Auth routes: POST /pam-login PAM flow (create/update PAM user, set is_pam_user, token), GET /pam-status, and GET /status now reports pamAvailable.
Frontend API, context & UI
src/utils/api.js, src/contexts/AuthContext.jsx, src/components/LoginForm.jsx
API: auth.pamLogin() and auth.pamStatus(). AuthContext: adds pamAvailable state and pamLogin() method, exposes them in provider. LoginForm: auth mode toggle (local / pam), UI/labels/placeholders adapt, uses pamLogin when PAM selected.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant L as LoginForm (UI)
  participant C as AuthContext
  participant API as api.auth.pamLogin
  participant R as /api/auth/pam-login
  participant PAM as PAMAuthService
  participant DB as userDb
  participant T as TokenGen

  U->>L: Enter username/password, select PAM
  L->>C: submit()
  C->>API: pamLogin(username, password)
  API->>R: POST /pam-login {username, password}
  R->>PAM: isAvailable()
  alt PAM unavailable
    R-->>API: 501 { pamAvailable: false }
    API-->>C: error (PAM unavailable)
  else PAM available
    R->>PAM: authenticate(username, password)
    alt success
      R->>DB: getUserByUsername -> createUser/updateUserPamStatus, update last_login
      R->>T: generate token
      R-->>API: 200 { token, user{..., is_pam_user} }
      API-->>C: token, user
      C-->>L: login success
    else failure
      R-->>API: 401/403
      API-->>C: auth failure
      C-->>L: show error
    end
  end
Loading
sequenceDiagram
  participant App as App
  participant C as AuthContext
  participant API as api.auth.pamStatus / status
  participant R as /api/auth/{status,pam-status}
  participant PAM as PAMAuthService

  App->>C: on load -> check status
  C->>API: GET /api/auth/status
  API->>R: /status
  R->>PAM: isAvailable()
  R-->>API: { pamAvailable: bool, ... }
  API-->>C: store pamAvailable
  C-->>App: enable PAM toggle if true
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble at the login door,
System keys now hop on board—
Toggle, sniff, a PAM-born friend,
Flags and hashes gently blend.
Carrots, tokens, paws in sync. 🥕🐇

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the primary change—adding PAM authentication support—and uses a standard “Feat:” prefix without extraneous details, making it immediately understandable to reviewers scanning the history.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/routes/auth.js (1)

95-104: Handle PAM users in the local /login path.

When a PAM-backed account exists, its password_hash is NULL. Hitting the legacy /login endpoint with that username now causes bcrypt.compare(password, user.password_hash) to throw because the hash is null, which drops the request into the catch block and returns a 500. That is exactly what happens if a PAM user forgets to flip the new UI toggle (the default is still “Local”). Please guard this case—either short-circuit with a 403 instructing the client to use PAM, or populate a password hash for PAM users—so we don’t regress into server errors on a valid but misrouted login attempt.

🧹 Nitpick comments (1)
server/database/db.js (1)

42-47: Align returned property casing with existing conventions.

createUser now returns an object containing is_pam_user, but elsewhere in the codebase (e.g., API responses and AuthContext) we expose camelCase (isPamUser). Mixing casing makes it easy to forget to normalize the value and can trigger subtle bugs when the raw createUser payload is reused. Please standardize on camelCase at the point of object construction (e.g., return { id: ..., username, isPamUser: isPamUser }) and adjust downstream consumers accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c9a4ca and 8624c62.

📒 Files selected for processing (7)
  • server/database/db.js (2 hunks)
  • server/database/init.sql (1 hunks)
  • server/routes/auth.js (2 hunks)
  • server/services/pamAuth.js (1 hunks)
  • src/components/LoginForm.jsx (3 hunks)
  • src/contexts/AuthContext.jsx (5 hunks)
  • src/utils/api.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
server/services/pamAuth.js (1)
server/database/db.js (1)
  • result (45-45)
src/contexts/AuthContext.jsx (2)
server/routes/auth.js (6)
  • pamAvailable (13-13)
  • pamAvailable (142-142)
  • pamAvailable (194-194)
  • token (57-57)
  • token (107-107)
  • token (169-169)
src/utils/api.js (3)
  • api (23-147)
  • api (23-147)
  • token (3-3)
src/utils/api.js (2)
src/components/LoginForm.jsx (2)
  • username (6-6)
  • password (7-7)
src/components/SetupForm.jsx (2)
  • username (6-6)
  • password (7-7)
src/components/LoginForm.jsx (2)
src/contexts/AuthContext.jsx (6)
  • error (31-31)
  • useAuth (17-23)
  • useAuth (17-23)
  • pamAvailable (30-30)
  • pamLogin (134-157)
  • login (85-107)
server/routes/auth.js (3)
  • pamAvailable (13-13)
  • pamAvailable (142-142)
  • pamAvailable (194-194)
server/routes/auth.js (3)
server/database/db.js (1)
  • userDb (30-100)
src/contexts/AuthContext.jsx (3)
  • pamAvailable (30-30)
  • user (26-26)
  • token (27-27)
server/middleware/auth.js (3)
  • user (34-34)
  • token (24-24)
  • generateToken (48-57)

Comment on lines +48 to +84
async authenticateWithSu(username, password) {
return new Promise((resolve) => {
const child = spawn('su', [username, '-c', 'exit'], {
stdio: ['pipe', 'pipe', 'pipe']
});

let output = '';
let error = '';

child.stdout.on('data', (data) => {
output += data.toString();
});

child.stderr.on('data', (data) => {
error += data.toString();
});

child.on('close', (code) => {
resolve(code === 0);
});

child.on('error', () => {
resolve(false);
});

// Send password to stdin
child.stdin.write(password + '\n');
child.stdin.end();

// Timeout after 5 seconds
setTimeout(() => {
if (!child.killed) {
child.kill();
resolve(false);
}
}, 5000);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

su path can authenticate everyone when the service runs as root
If this Node service ends up running as UID 0 (very common in containers or when started with sudo), su ... -c exit will always succeed regardless of the supplied password because PAM’s pam_rootok module lets root skip password checks entirely. That means a wrong password is accepted as soon as this branch is hit. We need to avoid using su (or wrap it in a helper that enforces PAM verification) when the caller is privileged, and instead call a real PAM binding that validates the target user’s credentials.(man7.org)

Comment on lines 132 to 157
async authenticateWithLogin(username, password) {
return new Promise((resolve) => {
// This is a fallback method that checks if the user exists
// Note: This doesn't actually verify the password, just checks user existence
const child = spawn('id', [username], {
stdio: ['pipe', 'pipe', 'pipe']
});

child.on('close', (code) => {
// If user exists, we'll assume authentication for now
// In a real implementation, you'd want to use a proper PAM module
resolve(code === 0);
});

child.on('error', () => {
resolve(false);
});

// Timeout after 3 seconds
setTimeout(() => {
if (!child.killed) {
child.kill();
resolve(false);
}
}, 3000);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fallback treats mere account existence as a successful login
The authenticateWithLogin branch just runs id username and returns true if the account exists. Any password—even empty—will therefore authenticate as long as the user is present on the system. This bypasses the entire password check and turns PAM auth into unconditional access. Please remove this fallback and replace it with an actual PAM check (e.g., via a proper PAM library or a helper binary that invokes pam_authenticate).

- Streamlined the authenticate method to use only the 'su' command for PAM authentication.
- Removed unused authentication methods (sudo and login) to reduce complexity.
- Updated isAvailable method to check only for the 'su' command availability.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
server/services/pamAuth.js (5)

58-64: Timeout/kill robustness

Killing only once after 5s may leave su lingering. Send SIGTERM, then escalate to SIGKILL after a short grace period, and ensure we don’t resolve twice.

-      // Timeout after 5 seconds
-      setTimeout(() => {
-        if (!child.killed) {
-          child.kill();
-          resolve(false);
-        }
-      }, 5000);
+      // Timeout after 5 seconds with two-phase kill
+      const timer = setTimeout(() => {
+        if (!child.killed) {
+          child.kill('SIGTERM');
+          setTimeout(() => {
+            if (!child.killed) child.kill('SIGKILL');
+          }, 250);
+          resolve(false);
+        }
+      }, 5000);
+      child.on('close', () => clearTimeout(timer));

73-85: isAvailable() checks the wrong capability

Presence of su doesn’t prove PAM auth viability for the intended service. If you keep serviceName, probe for /etc/pam.d/${serviceName} (or attempt a no-op PAM call) instead.

Possible approach:

  • Check fs.access('/etc/pam.d/' + this.serviceName) and return true only if readable.
  • Or prefer a real PAM library and use a lightweight probe call during startup.

90-121: Harden getUserInfo: validate fields and normalize types

Guard for malformed getent output and return numeric uid/gid. Current code may return strings and can mis-index on unexpected formats.

-      child.on('close', (code) => {
-        if (code === 0 && output) {
-          const parts = output.trim().split(':');
-          resolve({
-            username: parts[0],
-            uid: parts[2],
-            gid: parts[3],
-            name: parts[4],
-            home: parts[5],
-            shell: parts[6]
-          });
-        } else {
-          resolve(null);
-        }
-      });
+      child.on('close', (code) => {
+        if (code !== 0 || !output) return resolve(null);
+        const parts = output.trim().split(':');
+        if (parts.length < 7) return resolve(null);
+        const uid = Number.parseInt(parts[2], 10);
+        const gid = Number.parseInt(parts[3], 10);
+        resolve({
+          username: parts[0],
+          uid: Number.isNaN(uid) ? undefined : uid,
+          gid: Number.isNaN(gid) ? undefined : gid,
+          name: parts[4] || '',
+          home: parts[5] || '',
+          shell: parts[6] || ''
+        });
+      });

5-7: serviceName is unused

You set and expose serviceName but never use it in auth. Either wire it into the real PAM call or remove the property/mutator.

Also applies to: 127-129


1-3: Remove unused import

promisify isn’t used.

-import { promisify } from 'util';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8624c62 and 7a8dedf.

📒 Files selected for processing (1)
  • server/services/pamAuth.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/services/pamAuth.js (1)
server/database/db.js (1)
  • result (45-45)
🔇 Additional comments (1)
server/services/pamAuth.js (1)

31-66: Blocker: su-based “PAM” auth is insecure (root bypass) and nonfunctional without a TTY

  • If this process runs as UID 0, su username -c exit will succeed regardless of password (PAM’s pam_rootok), so wrong passwords authenticate.
  • Even when not root, su expects a controlling TTY; writing the password to stdin (Lines 55-56) won’t satisfy the PAM conversation, causing flaky failures/timeouts.

Replace this approach with a real PAM call (library or minimal setuid helper that invokes pam_authenticate). As an immediate mitigation, hard‑fail when running as root and stop attempting su auth.

Minimal mitigation (guard root + remove stdin write):

   async authenticateWithSu(username, password) {
     return new Promise((resolve) => {
-      const child = spawn('su', [username, '-c', 'exit'], {
+      // Hard-fail when privileged to avoid pam_rootok bypass
+      if (typeof process.getuid === 'function' && process.getuid() === 0) {
+        console.warn('[PAM] Refusing su-based auth while running as root');
+        return resolve(false);
+      }
+
+      const child = spawn('su', [username, '-c', 'exit'], {
         stdio: ['pipe', 'pipe', 'pipe']
       });
@@
-      // Send password to stdin
-      child.stdin.write(password + '\n');
-      child.stdin.end();
+      // su requires a TTY; stdin piping is ineffective and removed intentionally.

Preferred solution (switch to a proper PAM binding and use serviceName):

-import { spawn } from 'child_process';
-import { promisify } from 'util';
+import { spawn } from 'child_process';
+// Example: `authenticate-pam` (or similar) native binding
+import pam from 'authenticate-pam';

 class PAMAuthService {
@@
-  async authenticate(username, password) {
+  async authenticate(username, password) {
     try {
-      // Only use su command for PAM authentication
-      const result = await this.authenticateWithSu(username, password);
-      return result;
+      return await new Promise((resolve) => {
+        pam.authenticate(username, password, (err) => {
+          resolve(!err);
+        }, { serviceName: this.serviceName });
+      });
     } catch (error) {
       console.error('PAM authentication error:', error);
       return false;
     }
   }

Verification (ensure we’re not running as root in containers/PM2 configs):

#!/bin/bash
# Find container/runtime configs that set USER root (or omit non-root user)
fd -t f -a -E node_modules 'Dockerfile*' | xargs -I{} sh -c 'echo "== {} =="; rg -n -H "^\s*USER\s+" "{}" || true'
# Check for any explicit dropping of privileges in Node code
rg -nH "process\.(setuid|setgid|getuid)" -g '!**/node_modules/**'
# Map all PAM usage to confirm no other fallbacks exist
rg -nH "pamAuth|/pam-login|authenticateWithSu" -g '!**/node_modules/**'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant