1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2026-04-22 06:41:29 +00:00

Refactor the two authenticators into separate modules and add support for authorization

This commit is contained in:
Jermolene
2018-06-25 17:11:41 +01:00
parent 9f4c997292
commit 13f7959e63
4 changed files with 202 additions and 89 deletions

View File

@@ -2,6 +2,7 @@ title: $:/language/Docs/ModuleTypes/
allfilteroperator: A sub-operator for the ''all'' filter operator.
animation: Animations that may be used with the RevealWidget.
authenticator: Defines how requests are authenticated by the built-in HTTP server.
bitmapeditoroperation: A bitmap editor toolbar operation.
command: Commands that can be executed under Node.js.
config: Data to be inserted into `$tw.config`.

View File

@@ -0,0 +1,90 @@
/*\
title: $:/core/modules/server/authenticators/basic.js
type: application/javascript
module-type: authenticator
Authenticator for WWW basic authentication
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
if($tw.node) {
var util = require("util"),
fs = require("fs"),
url = require("url"),
path = require("path");
}
function BasicAuthenticator(server) {
this.server = server;
this.credentialsData = [];
}
/*
Returns true if the authenticator is active, false if it is inactive, or a string if there is an error
*/
BasicAuthenticator.prototype.init = function() {
// Read the credentials data
this.credentialsFilepath = this.server.get("credentials");
if(this.credentialsFilepath) {
var resolveCredentialsFilepath = path.join($tw.boot.wikiPath,this.credentialsFilepath);
if(fs.existsSync(resolveCredentialsFilepath) && !fs.statSync(resolveCredentialsFilepath).isDirectory()) {
var credentialsText = fs.readFileSync(resolveCredentialsFilepath,"utf8"),
credentialsData = $tw.utils.parseCsvStringWithHeader(credentialsText);
if(typeof credentialsData === "string") {
return "Error: " + credentialsData + " reading credentials from '" + resolveCredentialsFilepath + "'";
} else {
this.credentialsData = credentialsData;
}
} else {
return "Error: Unable to load user credentials from '" + credentialsFilepath + "'";
}
}
// Add the hardcoded username and password if specified
if(this.server.get("username") && this.server.get("password")) {
this.credentialsData = this.credentialsData || [];
this.credentialsData.push({
username: this.server.get("username"),
password: this.server.get("password")
});
}
return this.credentialsData.length > 0;
};
/*
Returns true if the request is authenticated and assigns the "authenticatedUsername" state variable.
Returns false if the request couldn't be authenticated having sent an appropriate response to the browser
*/
BasicAuthenticator.prototype.authenticateRequest = function(request,response,state) {
// Extract the incoming username and password from the request
var header = request.headers.authorization || "",
token = header.split(/\s+/).pop() || "",
auth = $tw.utils.base64Decode(token),
parts = auth.split(/:/),
incomingUsername = parts[0],
incomingPassword = parts[1];
// Check that at least one of the credentials matches
var matchingCredentials = this.credentialsData.find(function(credential) {
return credential.username === incomingUsername && credential.password === incomingPassword;
});
if(matchingCredentials) {
// If so, add the authenticated username to the request state
state.authenticatedUsername = incomingUsername;
return true;
} else {
// If not, return an authentication challenge
response.writeHead(401,"Authentication required",{
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + state.server.servername + '"'
});
response.end();
return false;
}
};
exports.AuthenticatorClass = BasicAuthenticator;
})();

View File

@@ -0,0 +1,47 @@
/*\
title: $:/core/modules/server/authenticators/header.js
type: application/javascript
module-type: authenticator
Authenticator for trusted header authentication
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
function HeaderAuthenticator(server) {
this.server = server;
this.header = server.get("authenticatedUserHeader");
}
/*
Returns true if the authenticator is active, false if it is inactive, or a string if there is an error
*/
HeaderAuthenticator.prototype.init = function() {
return !!this.header;
};
/*
Returns true if the request is authenticated and assigns the "authenticatedUsername" state variable.
Returns false if the request couldn't be authenticated having sent an appropriate response to the browser
*/
HeaderAuthenticator.prototype.authenticateRequest = function(request,response,state) {
// Otherwise, authenticate as the username in the specified header
var username = request.headers[this.header];
if(!username) {
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
response.writeHead(401,"Authorization header required to login to '" + state.server.servername + "'");
response.end();
return false;
} else {
state.authenticatedUsername = username;
return true;
}
};
exports.AuthenticatorClass = HeaderAuthenticator;
})();

View File

@@ -29,9 +29,30 @@ options: variables - optional hashmap of variables to set (a misnomer - they are
function Server(options) {
var self = this;
this.routes = options.routes || [];
this.authenticators = options.authenticators || [];
this.wiki = options.wiki;
this.variables = $tw.utils.extend({},this.defaultVariables,options.variables);
// Add route handlers
this.servername = this.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
// Initialise the variables
this.variables = $tw.utils.extend({},this.defaultVariables);
if(options.variables) {
for(var variable in options.variables) {
if(options.variables[variable]) {
this.variables[variable] = options.variables[variable];
}
}
}
$tw.utils.extend({},this.defaultVariables,options.variables);
// Initialise authorization
this.authorizationPrincipals = {
readers: (this.get("readers") || this.get("username") || "(anon)").split(",").map($tw.utils.trim),
writers: (this.get("writers") || this.get("username") || "(anon)").split(",").map($tw.utils.trim)
}
// Load and initialise authenticators
$tw.modules.forEachModuleOfType("authenticator", function(title,authenticatorDefinition) {
// console.log("Loading server route " + title);
self.addAuthenticator(authenticatorDefinition.AuthenticatorClass);
});
// Load route handlers
$tw.modules.forEachModuleOfType("route", function(title,routeDefinition) {
// console.log("Loading server route " + title);
self.addRoute(routeDefinition);
@@ -47,13 +68,6 @@ Server.prototype.defaultVariables = {
debugLevel: "none"
};
Server.prototype.set = function(obj) {
var self = this;
$tw.utils.each(obj,function(value,name) {
self.variables[name] = value;
});
};
Server.prototype.get = function(name) {
return this.variables[name];
};
@@ -62,6 +76,18 @@ Server.prototype.addRoute = function(route) {
this.routes.push(route);
};
Server.prototype.addAuthenticator = function(AuthenticatorClass) {
// Instantiate and initialise the authenticator
var authenticator = new AuthenticatorClass(this),
result = authenticator.init();
if(typeof result === "string") {
$tw.utils.error("Error: " + result);
} else if(result) {
// Only use the authenticator if it initialised successfully
this.authenticators.push(authenticator);
}
};
Server.prototype.findMatchingRoute = function(request,state) {
var pathprefix = this.get("pathprefix") || "";
for(var t=0; t<this.routes.length; t++) {
@@ -90,57 +116,13 @@ Server.prototype.findMatchingRoute = function(request,state) {
return null;
};
Server.prototype.authenticateRequestBasic = function(request,response,state) {
if(!this.credentialsData) {
// Authenticate as anonymous if no credentials have been specified
return true;
} else {
// Extract the incoming username and password from the request
var header = request.headers.authorization || "",
token = header.split(/\s+/).pop() || "",
auth = $tw.utils.base64Decode(token),
parts = auth.split(/:/),
incomingUsername = parts[0],
incomingPassword = parts[1];
// Check that at least one of the credentials matches
var matchingCredentials = this.credentialsData.find(function(credential) {
return credential.username === incomingUsername && credential.password === incomingPassword;
});
if(matchingCredentials) {
// If so, add the authenticated username to the request state
state.authenticatedUsername = incomingUsername;
return true;
} else {
// If not, return an authentication challenge
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
response.writeHead(401,"Authentication required",{
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + servername + '"'
});
response.end();
return false;
}
}
};
Server.prototype.authenticateRequestByHeader = function(request,response,state) {
var self = this,
header = self.get("authenticatedUserHeader")
if(!header) {
// Authenticate as anonymous if no trusted authenticated user header is specified
return true;
} else {
// Otherwise, authenticate as the username in the specified header
var username = request.headers[header];
if(!username) {
var servername = state.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5";
response.writeHead(401,"Authorization header required to login to '" + servername + "'");
response.end();
return false;
} else {
state.authenticatedUsername = username;
return true;
}
}
Server.prototype.methodMappings = {
"GET": "readers",
"OPTIONS": "readers",
"HEAD": "readers",
"PUT": "writers",
"POST": "writers",
"DELETE": "writers"
};
Server.prototype.requestHandler = function(request,response) {
@@ -150,12 +132,29 @@ Server.prototype.requestHandler = function(request,response) {
state.wiki = self.wiki;
state.server = self;
state.urlInfo = url.parse(request.url);
// Authenticate: provide error response on failure, add "username" to the state on success
if(!this.authenticateRequestBasic(request,response,state) || !this.authenticateRequestByHeader(request,response,state)) {
return;
// Get the principals authorized to access this resource
var principals = this.authorizationPrincipals[this.methodMappings[request.method] || "readers"] || [];
// Check whether anonymous access is enabled
if(principals.indexOf("(anon)") === -1) {
// Complain if there are no active authenticators
if(this.authenticators.length < 1) {
$tw.utils.error("Warning: Authentication required but no authentication modules are active");
response.writeHead(401,"Authentication required to login to '" + this.servername + "'");
response.end();
return;
}
// Authenticate
if(!this.authenticators[0].authenticateRequest(request,response,state)) {
// Bail if we failed (the authenticator will have sent the response)
return;
}
// Authorize with the authenticated username
if(principals.indexOf(state.authenticatedUsername) === -1) {
response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
response.end();
return;
}
}
// Authorize
// Find the route that matches this path
var route = self.findMatchingRoute(request,state);
// Optionally output debug info
@@ -211,30 +210,6 @@ Server.prototype.listen = function(port,host) {
if(!$tw.wiki.getTiddler("$:/plugins/tiddlywiki/tiddlyweb") || !$tw.wiki.getTiddler("$:/plugins/tiddlywiki/filesystem")) {
$tw.utils.warning("Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file");
}
// Read the credentials data if present
var credentialsFilepath = this.get("credentials");
if(credentialsFilepath) {
credentialsFilepath = path.join($tw.boot.wikiPath,credentialsFilepath);
if(fs.existsSync(credentialsFilepath) && !fs.statSync(credentialsFilepath).isDirectory()) {
var credentialsText = fs.readFileSync(credentialsFilepath,"utf8"),
credentialsData = $tw.utils.parseCsvStringWithHeader(credentialsText);
if(typeof credentialsData === "string") {
$tw.utils.error("Error: " + credentialsData + " reading credentials from '" + credentialsFilepath + "'");
} else {
this.credentialsData = credentialsData;
}
} else {
$tw.utils.error("Error: Unable to load user credentials from '" + credentialsFilepath + "'");
}
}
// Add the hardcoded username and password if specified
if(this.get("username") && this.get("password")) {
this.credentialsData = this.credentialsData || [];
this.credentialsData.push({
username: this.get("username"),
password: this.get("password")
});
}
return http.createServer(this.requestHandler.bind(this)).listen(port,host);
};