Files
sofarr/server/routes/auth.js
Gronod 1eadb30481 fix(security #6): add rate limiting to POST /api/auth/login
Uses express-rate-limit@6 (pinned for Node 12 dev compat; Node 18
in prod container is unaffected). Limits each IP to 10 attempts per
15-minute window. Returns 429 with a safe error message on breach.
2026-05-16 16:18:34 +01:00

109 lines
2.9 KiB
JavaScript

const express = require('express');
const axios = require('axios');
const rateLimit = require('express-rate-limit');
const router = express.Router();
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
standardHeaders: true,
legacyHeaders: false,
message: { success: false, error: 'Too many login attempts, please try again later' }
});
// Authenticate user with Emby
router.post('/login', loginLimiter, async (req, res) => {
try {
const { username, password } = req.body;
console.log(`[Auth] Attempting login for user: ${username}`);
// Authenticate with Emby
const authResponse = await axios.post(`${EMBY_URL}/Users/authenticatebyname`, {
Username: username,
Pw: password
}, {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="MediaDashboard", Device="Browser", DeviceId="dashboard-${Date.now()}", Version="1.0.0"`
}
});
const authData = authResponse.data;
// Get user info using the access token
const userResponse = await axios.get(`${EMBY_URL}/Users/${authData.User.Id || authData.User.id}`, {
headers: {
'X-MediaBrowser-Token': authData.AccessToken
}
});
const user = userResponse.data;
console.log(`[Auth] Login successful for user: ${user.Name}, isAdmin: ${!!(user.Policy && user.Policy.IsAdministrator)}`);
// Set authentication cookie
// Note: token is intentionally excluded from the cookie — it is not needed client-side
const isAdmin = !!(user.Policy && user.Policy.IsAdministrator);
res.cookie('emby_user', JSON.stringify({
id: user.Id,
name: user.Name,
isAdmin: isAdmin
}), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});
res.json({
success: true,
user: {
id: user.Id,
name: user.Name,
isAdmin: isAdmin
}
});
} catch (error) {
console.error(`[Auth] Login failed:`, error.message);
res.status(401).json({
success: false,
error: 'Invalid username or password'
});
}
});
// Get current authenticated user
router.get('/me', (req, res) => {
try {
const userCookie = req.cookies.emby_user;
if (!userCookie) {
return res.json({ authenticated: false });
}
const user = JSON.parse(userCookie);
res.json({
authenticated: true,
user: {
id: user.id,
name: user.name,
isAdmin: !!user.isAdmin
}
});
} catch (error) {
console.error(`[Auth] Error getting current user:`, error.message);
res.json({ authenticated: false });
}
});
// Logout
router.post('/logout', (req, res) => {
res.clearCookie('emby_user', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
});
res.json({ success: true });
});
module.exports = router;