Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mws authentication #8596

Open
wants to merge 20 commits into
base: multi-wiki-support
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion editions/multiwikiserver/tiddlywiki.info
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"tiddlywiki/tiddlyweb",
"tiddlywiki/filesystem",
"tiddlywiki/multiwikiclient",
"tiddlywiki/multiwikiserver"
"tiddlywiki/multiwikiserver",
"tiddlywiki/authentication"
],
"themes": [
"tiddlywiki/vanilla",
Expand Down
9 changes: 9 additions & 0 deletions plugins/tiddlywiki/authentication/plugin.info
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "$:/plugins/tiddlywiki/authentication",
webplusai marked this conversation as resolved.
Show resolved Hide resolved
"description": "Authentication plugin for TiddlyWiki",
"author": "Anon",
"version": "0.1.0",
"core-version": ">=5.0.0",
"plugin-type": "plugin",
"list": ["login"]
}
77 changes: 77 additions & 0 deletions plugins/tiddlywiki/authentication/tiddlers/login.tid
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
title: $:/plugins/tiddlywiki/authentication/login
tags: $:/tags/ServerRoute
route-method: GET
route-path: /login

\define loginForm()
<form class="login-form" method="POST" action="/login">
<input type="hidden" name="returnUrl" value=<<returnUrl>>/>
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Log In"/>
</form>
\end

<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.login-container {
max-width: 300px;
padding: 20px;
background-color: #ffffff;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.login-container h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.login-form input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.login-form input[type="submit"] {
background-color: #4CAF50;
color: white;
cursor: pointer;
border: none;
}
.login-form input[type="submit"]:hover {
background-color: #45a049;
}
.tc-error-message {
color: #ff0000;
text-align: center;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<h1>TiddlyWiki Login</h1>
<$set name="returnUrl" value={{{ [{$:/temp/mws/login/returnUrl}!is[blank]else{$:/info/url/query}split[returnUrl=]last[]else[/]] }}}>
webplusai marked this conversation as resolved.
Show resolved Hide resolved
<<loginForm>>
</$set>
<$list filter="[[$:/temp/mws/login/error]!is[missing]]" variable="errorTiddler">
<div class="tc-error-message">
{{$:/temp/mws/login/error}}
</div>
</$list>
</div>
</body>
</html>
150 changes: 141 additions & 9 deletions plugins/tiddlywiki/multiwikiserver/modules/mws-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ function Server(options) {
$tw.modules.forEachModuleOfType("mws-route", function(title,routeDefinition) {
self.addRoute(routeDefinition);
});
// Load tiddler-based routes
self.loadAuthRoutes();
// Initialise the http vs https
this.listenOptions = null;
this.protocol = "http";
Expand Down Expand Up @@ -301,6 +303,84 @@ Server.prototype.addRoute = function(route) {
this.routes.push(route);
};

Server.prototype.loadAuthRoutes = function () {
var self = this;
// add the login page route
self.addRoute({
method: "GET",
path: /^\/login$/,
handler: function (request, response, state) {
var loginTiddler = self.wiki.getTiddler("$:/plugins/tiddlywiki/authentication/login");
if (loginTiddler) {
var text = self.wiki.renderTiddler("text/html", loginTiddler.fields.title);
response.writeHead(200, { "Content-Type": "text/html" });
response.end(text);
} else {
response.writeHead(404);
response.end("Login page not found");
}
}
});
// add the login submission handler route
self.addRoute({
method: "POST",
path: /^\/login$/,
csrfDisable: true,
handler: function(request, response, state) {
self.handleLogin(request, response, state);
}.bind(self)
});
};

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 ?? '/');
response.writeHead(302, {
'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)}`);
// }
};

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;
};

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);
return sessionId;
};

Server.prototype.addAuthenticator = function(AuthenticatorClass) {
// Instantiate and initialise the authenticator
var authenticator = new AuthenticatorClass(this),
Expand Down Expand Up @@ -358,9 +438,50 @@ 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;
}

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")})

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

Server.prototype.requestAuthentication = function(response) {
if (!response.headersSent) {
response.writeHead(401, {
'WWW-Authenticate': 'Basic realm="Secure Area"'
});
response.end('Authentication required.');
}
};

Server.prototype.redirectToLogin = function(response, returnUrl) {
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);

// Compose the state object
var self = this;
var state = {};
Expand All @@ -374,43 +495,54 @@ 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.authenticatedUsername = authenticatedUsername;

// Get the principals authorized to access this resource
state.authorizationType = options.authorizationType || this.methodMappings[request.method] || "readers";

// Check whether anonymous access is granted
state.allowAnon = this.isAuthorized(state.authorizationType,null);
// Authenticate with the first active authenticator
if(this.authenticators.length > 0) {
if(!this.authenticators[0].authenticateRequest(request,response,state)) {
// Bail if we failed (the authenticator will have sent the response)
state.allowAnon = false;//this.isAuthorized(state.authorizationType,null);

// If not authenticated and anonymous access is not allowed, request authentication
if(!authenticatedUsername && !state.allowAnon) {
// Don't redirect if this is already a request to the login page
if(state.urlInfo.pathname !== '/login') {
this.redirectToLogin(response, request.url);
return;
}
}

// Authorize with the authenticated username
if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername)) {
response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername) && !response.headersSent) {
response.writeHead(403,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
response.end();
return;
}

// Find the route that matches this path
var route = self.findMatchingRoute(request,state);

// Optionally output debug info
if(self.get("debug-level") !== "none") {
console.log("Request path:",JSON.stringify(state.urlInfo));
console.log("Request headers:",JSON.stringify(request.headers));
console.log("authenticatedUsername:",state.authenticatedUsername);
}

// Return a 404 if we didn't find a route
if(!route) {
if(!route && !response.headersSent) {
response.writeHead(404);
response.end();
return;
}

// If this is a write, check for the CSRF header unless globally disabled, or disabled for this route
if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") {
if(!this.csrfDisable && !route.csrfDisable && state.authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki" && !response.headersSent) {
response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'");
response.end();
return;
}
if (response.headersSent) return;
// Receive the request body if necessary and hand off to the route handler
if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") {
// Let the route handle the request stream itself
Expand Down
Loading
Loading