Skip to content

Commit

Permalink
Implement user authentication as well as session handling
Browse files Browse the repository at this point in the history
  • Loading branch information
webplusai committed Sep 18, 2024
1 parent 4cb0379 commit c5bc0df
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 54 deletions.
127 changes: 73 additions & 54 deletions plugins/tiddlywiki/multiwikiserver/modules/mws-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function Server(options) {
this.authenticators = options.authenticators || [];
this.wiki = options.wiki;
this.boot = options.boot || $tw.boot;
this.sqlTiddlerDatabase = $tw.mws.store.sqlTiddlerDatabase;
// Initialise the variables
this.variables = $tw.utils.extend({},this.defaultVariables);
if(options.variables) {
Expand Down Expand Up @@ -310,6 +311,14 @@ Server.prototype.loadAuthRoutes = function () {
method: "GET",
path: /^\/login$/,
handler: function (request, response, state) {
// Check if the user already has a valid session
const authenticatedUser = self.authenticateUser(request, response);
if (authenticatedUser) {
// User is already logged in, redirect to home page
response.writeHead(302, { 'Location': '/' });
response.end();
return;
}
var loginTiddler = self.wiki.getTiddler("$:/plugins/tiddlywiki/authentication/login");
if (loginTiddler) {
var text = self.wiki.renderTiddler("text/html", loginTiddler.fields.title);
Expand All @@ -332,52 +341,46 @@ Server.prototype.loadAuthRoutes = function () {
});
};

Server.prototype.handleLogin = function(request, response, state) {
Server.prototype.handleLogin = function (request, response, state) {
var self = this;
const querystring = require('querystring');
const formData = querystring.parse(state.data);
const { username, password, returnUrl } = formData;

console.log("Parsed form data:", formData);

// Use the SQL method to get the user
// const user = $tw.mws.sqlTiddlerDatabase.getUserByUsername(username);
// console.log("USER =>", username, user);

// if(user && self.verifyPassword(password, user.password_hash)) {
// // Authentication successful
// const sessionId = self.createSession(user.user_id);
// response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
// state.redirect(returnUrl ?? '/');
const { username, password } = formData;
const user = self.sqlTiddlerDatabase.getUserByUsername(username);
const isPasswordValid = self.verifyPassword(password, user?.password)

if (user && isPasswordValid) {
const sessionId = self.createSession(user.user_id);
const {returnUrl} = this.parseCookieString(request.headers.cookie)
response.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Path=/`);
response.writeHead(302, {
'Location': '/'//returnUrl ?? '/'
'Location': returnUrl || '/'
});
response.end();
// } else {
// // Authentication failed
// self.wiki.addTiddler(new $tw.Tiddler({
// title: "$:/temp/mws/login/error",
// text: "Invalid username or password"
// }));
// state.redirect(`/login?returnUrl=${encodeURIComponent(returnUrl)}`);
// }
} else {
this.wiki.addTiddler(new $tw.Tiddler({
title: "$:/temp/mws/login/error",
text: errorMessage
}));
response.writeHead(302, {
'Location': '/login'
});
}
response.end();
};

Server.prototype.verifyPassword = function(inputPassword, storedHash) {
// Implement password verification logic here
// This depends on how you've stored the passwords (e.g., bcrypt, argon2)
// For example, using bcrypt:
// return bcrypt.compareSync(inputPassword, storedHash);

// Placeholder implementation (NOT SECURE, replace with proper verification):
return inputPassword === storedHash;
const hashedInput = this.hashPassword(inputPassword);
return hashedInput === storedHash;
};

Server.prototype.hashPassword = function(password) {
return crypto.createHash('sha256').update(password).digest('hex');
};

Server.prototype.createSession = function(userId) {
const sessionId = crypto.randomBytes(16).toString('hex');
// Store the session in your database or in-memory store
// For example:
// this.sqlTiddlerDatabase.createSession(sessionId, userId);
this.sqlTiddlerDatabase.createOrUpdateUserSession(userId, sessionId);
return sessionId;
};

Expand Down Expand Up @@ -438,28 +441,39 @@ Server.prototype.isAuthorized = function(authorizationType,username) {
return principals.indexOf("(anon)") !== -1 || (username && (principals.indexOf("(authenticated)") !== -1 || principals.indexOf(username) !== -1));
}

Server.prototype.authenticateUser = function(request, response) {
const authHeader = request.headers.authorization;
if(!authHeader) {
this.requestAuthentication(response);
return false;
}
Server.prototype.parseCookieString = function(cookieString) {
const cookies = {};
if (typeof cookieString !== 'string') return cookies;

const auth = Buffer.from(authHeader.split(' ')[1], 'base64').toString().split(':');
const username = auth[0];
const password = auth[1];
// console.log({authHeader, auth, username, password, setUsername: this.get("username"), setPassword: this.get("password")})
cookieString.split(';').forEach(cookie => {
const parts = cookie.split('=');
if (parts.length >= 2) {
const key = parts[0].trim();
const value = parts.slice(1).join('=').trim();
cookies[key] = decodeURIComponent(value);
}
});

// Check if the username and password match the configured credentials
if(username === this.get("username") && password === this.get("password")) {
return username;
}else{
return cookies;
}

Server.prototype.authenticateUser = function(request, response) {
const {session: session_id} = this.parseCookieString(request.headers.cookie)
if (!session_id) {
return false;
}
// get user info
const user = this.sqlTiddlerDatabase.findUserBySessionId(session_id);
if (!user) {
return false
}
delete user.password;

return user
};

Server.prototype.requestAuthentication = function(response) {
if (!response.headersSent) {
if(!response.headersSent) {
response.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Secure Area"'
});
Expand All @@ -468,19 +482,23 @@ Server.prototype.requestAuthentication = function(response) {
};

Server.prototype.redirectToLogin = function(response, returnUrl) {
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
response.writeHead(302, {
'Location': loginUrl
});
response.end();
if(!response.headersSent) {
response.setHeader('Set-Cookie', `returnUrl=${returnUrl}; HttpOnly; Path=/`);
const loginUrl = '/login?returnUrl=' + encodeURIComponent(returnUrl);
response.writeHead(302, {
'Location': loginUrl
});
response.end();
}
};

Server.prototype.requestHandler = function(request,response,options) {
options = options || {};
const queryString = require("querystring");

// Authenticate the user
const authenticatedUsername = this.authenticateUser(request, response);
const authenticatedUser = this.authenticateUser(request, response);
const authenticatedUsername = authenticatedUser?.username;

// Compose the state object
var self = this;
Expand All @@ -495,6 +513,7 @@ Server.prototype.requestHandler = function(request,response,options) {
state.redirect = redirect.bind(self,request,response);
state.streamMultipartData = streamMultipartData.bind(self,request);
state.makeTiddlerEtag = makeTiddlerEtag.bind(self);
state.authenticatedUser = authenticatedUser;
state.authenticatedUsername = authenticatedUsername;

// Get the principals authorized to access this resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,20 @@ SqlTiddlerDatabase.prototype.createTables = function() {
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
last_login TEXT
)
`,`
-- User Session table
CREATE TABLE IF NOT EXISTS sessions (
user_id INTEGER NOT NULL,
session_id TEXT NOT NULL,
created_at TEXT NOT NULL,
last_accessed TEXT NOT NULL,
PRIMARY KEY (user_id),
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
`,`
-- Groups table
CREATE TABLE IF NOT EXISTS groups (
Expand Down Expand Up @@ -765,6 +776,92 @@ SqlTiddlerDatabase.prototype.listUsers = function() {
`);
};

SqlTiddlerDatabase.prototype.createOrUpdateUserSession = function(userId, sessionId) {
const currentTimestamp = new Date().toISOString();

// First, try to update an existing session
const updateResult = this.engine.runStatement(`
UPDATE sessions
SET session_id = $sessionId, last_accessed = $timestamp
WHERE user_id = $userId
`, {
$userId: userId,
$sessionId: sessionId,
$timestamp: currentTimestamp
});

// If no existing session was updated, create a new one
if (updateResult.changes === 0) {
this.engine.runStatement(`
INSERT INTO sessions (user_id, session_id, created_at, last_accessed)
VALUES ($userId, $sessionId, $timestamp, $timestamp)
`, {
$userId: userId,
$sessionId: sessionId,
$timestamp: currentTimestamp
});
}

return sessionId;
};

SqlTiddlerDatabase.prototype.findUserBySessionId = function(sessionId) {
// First, get the user_id from the sessions table
const sessionResult = this.engine.runStatementGet(`
SELECT user_id, last_accessed
FROM sessions
WHERE session_id = $sessionId
`, {
$sessionId: sessionId
});

if (!sessionResult) {
return null; // Session not found
}

const lastAccessed = new Date(sessionResult.last_accessed);
const expirationTime = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
if (new Date() - lastAccessed > expirationTime) {
// Session has expired
this.deleteSession(sessionId);
return null;
}

// Update the last_accessed timestamp
const currentTimestamp = new Date().toISOString();
this.engine.runStatement(`
UPDATE sessions
SET last_accessed = $timestamp
WHERE session_id = $sessionId
`, {
$sessionId: sessionId,
$timestamp: currentTimestamp
});

const userResult = this.engine.runStatementGet(`
SELECT *
FROM users
WHERE user_id = $userId
`, {
$userId: sessionResult.user_id
});

if (!userResult) {
return null;
}

return userResult;
};

SqlTiddlerDatabase.prototype.deleteSession = function(sessionId) {
this.engine.runStatement(`
DELETE FROM sessions
WHERE session_id = $sessionId
`, {
$sessionId: sessionId
});
};

// Group CRUD operations
SqlTiddlerDatabase.prototype.createGroup = function(groupName, description) {
const result = this.engine.runStatement(`
Expand Down

0 comments on commit c5bc0df

Please sign in to comment.