I'm new to node.js and I've created a login system. My app is in the subdirectory of a domain, for example: https://domain.tld/nrjs/. There are two folders within this app which contain static files. One folder is called public
and one is called protected
. When the user connects to the node app at https://domain.tld/nrjs/, they are served the static folder public
and directed to the login.html
page. Once they are authenticated they are then served the static folder protected
, and sent to https://domain.tld/nrjs/index.html which is stored in the protected
folder. The issue is, it seems whether the app.js sends them the folder or not (whether they have authentication or not), the user can directly link to the folder at https://domain.tld/nrjs/protected/index.html or any other actual file, and it completely ignores any route handlers in my app.js file. I'm using cPanel. Below is my app.js, but it seems like the user can directly link to the file and app.js isn't even involved. I need to be able to prevent the user from accessing the protected
folder until they have successfully logged in, or preventing them from accessing it at all is fine too, as I will be sending them the files at /index.html
not /protected/index.html
.
Structure:
public_html/
│
└───nrjs/ (node.js app is in subdirectory)
│ app.js
│
├───public/
│ login.html (Publicly accessible login page)
│
└───protected/
index.html (Private home page, should not be directly accessible)
Here is my app.js:
const express = require('express');
const fs = require('fs');
const path = require('path');
const session = require('express-session');
const bodyParser = require('body-parser');
const bcrypt = require('bcryptjs');
const app = express();
const basePath = '/nrjs';
const saltRounds = 10;
let users = [
{ username: 'admin', password: 'password' },
{ username: 'user1', password: 'password1' },
{ username: 'user2', password: 'password2' }
];
Promise.all(users.map(user =>
bcrypt.hash(user.password, saltRounds).then(hash => {
user.password = hash;
})
)).then(() => {
console.log('Users initialized with hashed passwords');
});
app.use(session({
secret: 'KAL783BB4HJK1P90V3',
resave: false,
saveUninitialized: true
}));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(basePath, express.static(path.join(__dirname, 'public')));
function checkAuthenticated(req, res, next) {
if (!req.session.user && req.path !== '/login.html' && req.path !== '/login') {
return res.redirect(basePath + '/login.html');
}
next();
}
app.use(basePath, checkAuthenticated, express.static(path.join(__dirname, 'protected')));
app.post(basePath + '/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (user) {
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) {
console.error('Error during password comparison:', err);
return res.redirect(basePath + '/login.html?error=An error occurred');
}
if (isMatch) {
req.session.user = user.username;
const loginLog = `Successful login by ${user.username} at ${new Date().toLocaleString()}\n`;
fs.appendFile('logs/success_login_attempts.log', loginLog, (err) => {
if (err) {
console.error('Error writing to login log file:', err);
}
});
res.redirect(basePath + '/index.html');
} else {
const failedLoginLog = `Failed login attempt with username: ${username} at ${new Date().toLocaleString()}\n`;
fs.appendFile('logs/failed_login_attempts.log', failedLoginLog, (err) => {
if (err) {
console.error('Error writing to failed login log file:', err);
}
});
res.redirect(basePath + '/login.html?error=Incorrect username or password');
}
});
} else {
const failedLoginLog = `Failed login attempt with username: ${username} at ${new Date().toLocaleString()}\n`;
fs.appendFile('logs/failed_login_attempts.log', failedLoginLog, (err) => {
if (err) {
console.error('Error writing to failed login log file:', err);
}
});
res.redirect(basePath + '/login.html?error=Incorrect username or password');
}
});
app.get(basePath + '/login', (req, res) => {
res.redirect(basePath + '/login.html');
});
app.get(basePath + '/logout', (req, res) => {
req.session.destroy();
res.redirect(basePath + '/login.html');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
I've tried putting all kinds of route handlers at the top of the page, and did a catch-all route handler and it still let the user directly navigate to the file.
Example:
app.get('/nrjs/protected', (req, res) => {
res.status(403).send('Access Denied');
});
I also tried using a route wildcard:
app.get('/nrjs/protected/*', (req, res) => {
res.status(403).send('Access Denied');
});
and when trying to catch everything they can still navigate to the protected file:
app.use((req, res, next) => {
res.status(404).send("Sorry, the page you're looking for doesn't exist.");
});
I have also tried with blocking access to the protected
folder using .htaccess
and got a 503 error on routes outside of the protected
folder.
Edit: I've moved the node app outside of the web directory, and therefore the user's can no longer directly navigate to the apps static files, fixing the issue. However, I'd still like to know if there is a way to prevent the user from navigating the files if I did store the node app within the web directory. I can confirm that there is no session set when the user successfully navigates to the protected directory. Thanks!
All of the files that can be accessed via HTTP such as .jpg
, .css
, .js
, .txt
or .html
that are in your public
or protected
directory can be accessed in the browser simply by navigating to them in the url. This because whatever is serving files from the public_html/
directory (Apache likely) is the one serving up protected/index.html
and not express. Here is an illustration.
This is your domain:
https://domain.tld
User makes a GET
request to this file:
https://domain.tld/pancakes.html
Apache (for arguments sake) serves up the file from here:
/var/www/html/public_html/pancakes.html
User then makes a GET
request to this route for example:
https://domain.tld/nrjs/login
Well you must have something listening for traffic on port 443 to https://domain.tld/nrjs
and routing it to port 3000 so that express can pick it up.
When express gets that request on port 3000 it then sends a redirect
to nrjs/login.html
. Well you are using the app.use(basePath, express.static(path.join(__dirname, 'public')));
middleware so express sees login.html
in the public
folder and just sends the html back to the user.
Next the user sends a POST
request to here:
https://domain.tld/nrjs/login
Again its re-routed to port 3000 and after a few password validations etc express redirects
to nrjs/index.html
. It passes through the static
middleware looking at the public
directory and can't find it. Then it passes through the static
middleware looking at the protected
directory and bingo, it serves up index.html
.
Now what you have is a checkAuthenticated
middleware making sure it's an authenticated user looking for index.html
but that will only work for requests made on port 3000 to https://domain.tld/nrjs/index.html
.
If the user navigates to:
https://domain.tld/nrjs/protected/index.html
Then that it looks like Apache is just picking that up on port 443 and serving it up. Express doesn't have any route handlers for protected/index.html
, it only has nrjs/index.html
.
My advice would be:
protected
folder all together.index.html
to the same directory as app.js
.res.redirect(basePath + '/index.html');
to this
res.sendFile(__dirname + '/index.html')
app.use(basePath, checkAuthenticated, express.static(path.join(__dirname, 'protected')));
app.get(basePath + '/index', checkAuthenticated, (req, res) => {
res.sendFile(__dirname + '/index.html')
});
Now the only way a user can access index.html
is by going through the login process or by going through the https://domain.tld/nrjs/index
route which they will need to pass the checkAuthenticated
middleware first.