Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions server/database/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ const userDb = {
},

// Create a new user
createUser: (username, passwordHash) => {
createUser: (username, passwordHash, isPamUser = false) => {
try {
const stmt = db.prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
const result = stmt.run(username, passwordHash);
return { id: result.lastInsertRowid, username };
const stmt = db.prepare('INSERT INTO users (username, password_hash, is_pam_user) VALUES (?, ?, ?)');
const result = stmt.run(username, passwordHash, isPamUser ? 1 : 0);
return { id: result.lastInsertRowid, username, is_pam_user: isPamUser };
} catch (err) {
throw err;
}
Expand Down Expand Up @@ -71,11 +71,31 @@ const userDb = {
// Get user by ID
getUserById: (userId) => {
try {
const row = db.prepare('SELECT id, username, created_at, last_login FROM users WHERE id = ? AND is_active = 1').get(userId);
const row = db.prepare('SELECT id, username, created_at, last_login, is_pam_user FROM users WHERE id = ? AND is_active = 1').get(userId);
return row;
} catch (err) {
throw err;
}
},

// Update user PAM status
updateUserPamStatus: (userId, isPamUser) => {
try {
const stmt = db.prepare('UPDATE users SET is_pam_user = ? WHERE id = ?');
stmt.run(isPamUser ? 1 : 0, userId);
} catch (err) {
throw err;
}
},

// Check if user is PAM user
isPamUser: (userId) => {
try {
const row = db.prepare('SELECT is_pam_user FROM users WHERE id = ? AND is_active = 1').get(userId);
return row ? row.is_pam_user === 1 : false;
} catch (err) {
throw err;
}
}
};

Expand Down
3 changes: 2 additions & 1 deletion server/database/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
password_hash TEXT, -- Can be null for PAM users
is_pam_user BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
is_active BOOLEAN DEFAULT 1
Expand Down
79 changes: 78 additions & 1 deletion server/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import express from 'express';
import bcrypt from 'bcrypt';
import { userDb, db } from '../database/db.js';
import { generateToken, authenticateToken } from '../middleware/auth.js';
import pamAuth from '../services/pamAuth.js';

const router = express.Router();

// Check auth status and setup requirements
router.get('/status', async (req, res) => {
try {
const hasUsers = await userDb.hasUsers();
res.json({
const pamAvailable = await pamAuth.isAvailable();
res.json({
needsSetup: !hasUsers,
pamAvailable,
isAuthenticated: false // Will be overridden by frontend if token exists
});
} catch (error) {
Expand Down Expand Up @@ -125,6 +128,80 @@ router.get('/user', authenticateToken, (req, res) => {
});
});

// PAM authentication endpoint
router.post('/pam-login', async (req, res) => {
try {
const { username, password } = req.body;

// Validate input
if (!username || !password) {
return res.status(400).json({ error: 'Username and password are required' });
}

// Check if PAM is available
const pamAvailable = await pamAuth.isAvailable();
if (!pamAvailable) {
return res.status(501).json({ error: 'PAM authentication is not available on this system' });
}

// Authenticate using PAM
const isAuthenticated = await pamAuth.authenticate(username, password);

if (!isAuthenticated) {
return res.status(401).json({ error: 'Invalid username or password' });
}

// Get user info from system
const userInfo = await pamAuth.getUserInfo(username);

// Create or get user from database
let user = userDb.getUserByUsername(username);

if (!user) {
// Create new user in database if doesn't exist
user = userDb.createUser(username, null, true); // true for PAM user
} else {
// Update user to be PAM authenticated
userDb.updateUserPamStatus(user.id, true);
}

// Generate token
const token = generateToken(user);

// Update last login
userDb.updateLastLogin(user.id);

res.json({
success: true,
user: {
id: user.id,
username: user.username,
userInfo: userInfo,
isPamUser: true
},
token
});

} catch (error) {
console.error('PAM login error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Check PAM availability
router.get('/pam-status', async (req, res) => {
try {
const pamAvailable = await pamAuth.isAvailable();
res.json({
pamAvailable,
message: pamAvailable ? 'PAM authentication is available' : 'PAM authentication is not available'
});
} catch (error) {
console.error('PAM status check error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});

// Logout (client-side token removal, but this endpoint can be used for logging)
router.post('/logout', authenticateToken, (req, res) => {
// In a simple JWT system, logout is mainly client-side
Expand Down
132 changes: 132 additions & 0 deletions server/services/pamAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { spawn } from 'child_process';
import { promisify } from 'util';

class PAMAuthService {
constructor() {
this.serviceName = 'login'; // Default PAM service
}

/**
* Authenticate a user using PAM via system commands
* @param {string} username - The username to authenticate
* @param {string} password - The password to verify
* @returns {Promise<boolean>} True if authentication succeeds
*/
async authenticate(username, password) {
try {
// Only use su command for PAM authentication
const result = await this.authenticateWithSu(username, password);
return result;
} catch (error) {
console.error('PAM authentication error:', error);
return false;
}
}

/**
* Authenticate using su command
*/
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);
});
Comment on lines +29 to +65
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)

}



/**
* Check if PAM authentication is available on this system
*/
async isAvailable() {
try {
// Check if 'su' command is available (only command we need)
const child = spawn('which', ['su']);
await new Promise((resolve) => {
child.on('close', resolve);
});

return child.exitCode === 0;
} catch (error) {
return false;
}
}

/**
* Get user information from system
*/
async getUserInfo(username) {
return new Promise((resolve) => {
const child = spawn('getent', ['passwd', username], {
stdio: ['pipe', 'pipe', 'pipe']
});

let output = '';

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

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('error', () => {
resolve(null);
});
});
}

/**
* Set the PAM service name
*/
setServiceName(serviceName) {
this.serviceName = serviceName;
}
}

export default new PAMAuthService();
Loading