Cory McWilliams
5e72b111d9
git-svn-id: https://www.unprompted.com/svn/projects/tildefriends/trunk@3993 ed5197a5-7fde-0310-b194-c3ffbd925b24
270 lines
8.7 KiB
JavaScript
270 lines
8.7 KiB
JavaScript
import * as core from './core.js';
|
|
import * as http from './http.js';
|
|
import * as form from './form.js';
|
|
|
|
var gTokens = {};
|
|
var gDatabase = new Database("auth");
|
|
|
|
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
|
|
|
function b64url(value) {
|
|
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
|
let equals = value.indexOf('=');
|
|
if (equals !== -1) {
|
|
return value.substring(0, equals);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function unb64url(value) {
|
|
value = value.replaceAll('-', '+').replaceAll('_', '/');
|
|
let remainder = value.length % 4;
|
|
if (remainder == 3) {
|
|
return value + '=';
|
|
} else if (remainder == 2) {
|
|
return value + '==';
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
function makeJwt(payload) {
|
|
let ids = ssb.getIdentities(':auth');
|
|
let id;
|
|
if (ids?.length) {
|
|
id = ids[0];
|
|
} else {
|
|
id = ssb.createIdentity(':auth');
|
|
}
|
|
|
|
let final_payload = b64url(base64Encode(JSON.stringify(Object.assign({}, payload, {exp: (new Date().valueOf()) + kRefreshInterval}))));
|
|
let jwt = [b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))), final_payload, b64url(ssb.hmacsha256sign(final_payload, ':auth', id))].join('.');
|
|
return jwt;
|
|
}
|
|
|
|
function readSession(session) {
|
|
let jwt_parts = session?.split('.');
|
|
if (jwt_parts?.length === 3) {
|
|
let [header, payload, signature] = jwt_parts;
|
|
header = JSON.parse(base64Decode(unb64url(header)));
|
|
if (header.typ === 'JWT' && header.alg === 'HS256') {
|
|
signature = unb64url(signature);
|
|
let id = ssb.getIdentities(':auth');
|
|
if (id?.length && ssb.hmacsha256verify(id[0], payload, signature)) {
|
|
let result = JSON.parse(base64Decode(unb64url(payload)));
|
|
let now = new Date().valueOf()
|
|
if (now < result.exp) {
|
|
print(`JWT valid for another ${(result.exp - now) / 1000} seconds.`);
|
|
return result;
|
|
} else {
|
|
print(`JWT expired by ${(now - result.exp) / 1000} seconds.`);
|
|
}
|
|
} else {
|
|
print('JWT verification failed.');
|
|
}
|
|
} else {
|
|
print('Invalid JWT header.');
|
|
}
|
|
} else {
|
|
print('No session JWT.');
|
|
}
|
|
}
|
|
|
|
function verifyPassword(password, hash) {
|
|
return bCrypt.hashpw(password, hash) == hash;
|
|
}
|
|
|
|
function hashPassword(password) {
|
|
var salt = bCrypt.gensalt(12);
|
|
return bCrypt.hashpw(password, salt);
|
|
}
|
|
|
|
function noAdministrator() {
|
|
return !core.globalSettings || !core.globalSettings.permissions || !Object.keys(core.globalSettings.permissions).some(function(name) {
|
|
return core.globalSettings.permissions[name].indexOf("administration") != -1;
|
|
});
|
|
}
|
|
|
|
function makeAdministrator(name) {
|
|
if (!core.globalSettings.permissions) {
|
|
core.globalSettings.permissions = {};
|
|
}
|
|
if (!core.globalSettings.permissions[name]) {
|
|
core.globalSettings.permissions[name] = [];
|
|
}
|
|
if (core.globalSettings.permissions[name].indexOf("administration") == -1) {
|
|
core.globalSettings.permissions[name].push("administration");
|
|
}
|
|
core.setGlobalSettings(core.globalSettings);
|
|
}
|
|
|
|
function getCookies(headers) {
|
|
var cookies = {};
|
|
|
|
if (headers.cookie) {
|
|
var parts = headers.cookie.split(/,|;/);
|
|
for (var i in parts) {
|
|
var equals = parts[i].indexOf("=");
|
|
var name = parts[i].substring(0, equals).trim();
|
|
var value = parts[i].substring(equals + 1).trim();
|
|
cookies[name] = value;
|
|
}
|
|
}
|
|
|
|
return cookies;
|
|
}
|
|
|
|
function handler(request, response) {
|
|
var session = getCookies(request.headers).session;
|
|
if (request.uri == "/login") {
|
|
var sessionIsNew = false;
|
|
var loginError;
|
|
|
|
var formData = form.decodeForm(request.query);
|
|
|
|
if (request.method == "POST" || formData.submit) {
|
|
sessionIsNew = true;
|
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
|
if (formData.submit == "Login") {
|
|
var account = gDatabase.get("user:" + formData.name);
|
|
account = account ? JSON.parse(account) : account;
|
|
if (formData.register == "1") {
|
|
if (!account &&
|
|
formData.password == formData.confirm) {
|
|
let users = new Set();
|
|
let users_original = gDatabase.get('users');
|
|
try {
|
|
users = new Set(JSON.parse(users_original));
|
|
} catch {
|
|
}
|
|
if (!users.has(formData.name)) {
|
|
users.add(formData.name);
|
|
}
|
|
users = JSON.stringify([...users].sort());
|
|
if (users !== users_original) {
|
|
gDatabase.set('users', users);
|
|
}
|
|
session = makeJwt({name: formData.name});
|
|
account = {password: hashPassword(formData.password)};
|
|
gDatabase.set("user:" + formData.name, JSON.stringify(account));
|
|
if (noAdministrator()) {
|
|
makeAdministrator(formData.name);
|
|
}
|
|
} else {
|
|
loginError = "Error registering account.";
|
|
}
|
|
} else {
|
|
if (account &&
|
|
account.password &&
|
|
verifyPassword(formData.password, account.password)) {
|
|
session = makeJwt({name: formData.name});
|
|
if (noAdministrator()) {
|
|
makeAdministrator(formData.name);
|
|
}
|
|
} else {
|
|
loginError = "Invalid username or password.";
|
|
}
|
|
}
|
|
} else {
|
|
// Proceed as Guest
|
|
session = makeJwt({name: 'guest'});
|
|
}
|
|
}
|
|
|
|
var cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; Secure; SameSite=Strict`;
|
|
var entry = readSession(session);
|
|
if (entry && formData.return) {
|
|
response.writeHead(303, {"Location": formData.return, "Set-Cookie": cookie});
|
|
response.end();
|
|
} else {
|
|
File.readFile("core/auth.html").then(function(data) {
|
|
var html = utf8Decode(data);
|
|
var contents = "";
|
|
|
|
if (entry) {
|
|
if (sessionIsNew) {
|
|
contents += '<div>Welcome back, ' + entry.name + '.</div>\n';
|
|
} else {
|
|
contents += '<div>You are already logged in, ' + entry.name + '.</div>\n';
|
|
}
|
|
contents += '<div><a href="/login/logout">Logout</a></div>\n';
|
|
} else {
|
|
contents += '<form method="POST">\n';
|
|
if (loginError) {
|
|
contents += "<p>" + loginError + "</p>\n";
|
|
}
|
|
contents += '<div id="auth_greeting"><b>Halt. Who goes there?</b></div>\n'
|
|
contents += '<div id="auth">\n';
|
|
contents += '<div id="auth_login">\n'
|
|
if (noAdministrator()) {
|
|
contents += '<div class="notice">There is currently no administrator. You will be made administrator.</div>\n';
|
|
}
|
|
contents += '<div><label for="name">Name:</label> <input type="text" id="name" name="name" value=""></div>\n';
|
|
contents += '<div><label for="password">Password:</label> <input type="password" id="password" name="password" value=""></div>\n';
|
|
contents += '<div id="confirmPassword" style="display: none"><label for="confirm">Confirm:</label> <input type="password" id="confirm" name="confirm" value=""></div>\n';
|
|
contents += '<div><input type="checkbox" id="register" name="register" value="1" onchange="showHideConfirm()"> <label for="register">Register a new account</label></div>\n';
|
|
contents += '<div><input id="loginButton" type="submit" name="submit" value="Login"></div>\n';
|
|
contents += '</div>';
|
|
contents += '<div class="auth_or"> - or - </div>';
|
|
contents += '<div id="auth_guest">\n';
|
|
contents += '<input id="guestButton" type="submit" name="submit" value="Proceeed as Guest">\n';
|
|
contents += '</div>\n';
|
|
contents += '</div>\n';
|
|
contents += '</form>';
|
|
}
|
|
var text = html.replace("<!--SESSION-->", contents);
|
|
response.writeHead(200, {"Content-Type": "text/html; charset=utf-8", "Set-Cookie": cookie, "Content-Length": text.length});
|
|
response.end(text);
|
|
}).catch(function(error) {
|
|
response.writeHead(404, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
|
response.end("404 File not found");
|
|
});
|
|
}
|
|
} else if (request.uri == "/login/logout") {
|
|
response.writeHead(303, {"Set-Cookie": "session=; path=/; secure; SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT", "Location": "/login" + (request.query ? "?" + request.query : "")});
|
|
response.end();
|
|
} else {
|
|
response.writeHead(200, {"Content-Type": "text/plain; charset=utf-8", "Connection": "close"});
|
|
response.end("Hello, " + request.client.peerName + ".");
|
|
}
|
|
}
|
|
|
|
function getPermissions(session) {
|
|
var permissions;
|
|
var entry = readSession(session);
|
|
if (entry) {
|
|
permissions = getPermissionsForUser(entry.name);
|
|
permissions.authenticated = entry.name !== "guest";
|
|
}
|
|
return permissions || {};
|
|
}
|
|
|
|
function getPermissionsForUser(userName) {
|
|
var permissions = {};
|
|
if (core.globalSettings && core.globalSettings.permissions && core.globalSettings.permissions[userName]) {
|
|
for (var i in core.globalSettings.permissions[userName]) {
|
|
permissions[core.globalSettings.permissions[userName][i]] = true;
|
|
}
|
|
}
|
|
return permissions;
|
|
}
|
|
|
|
function query(headers) {
|
|
var session = getCookies(headers).session;
|
|
var entry;
|
|
var autologin = tildefriends.args.autologin;
|
|
if (entry = autologin ? {name: autologin} : readSession(session)) {
|
|
return {
|
|
session: entry,
|
|
permissions: autologin ? getPermissionsForUser(autologin) : getPermissions(session),
|
|
refresh: {
|
|
token: makeJwt({name: entry.name}),
|
|
interval: kRefreshInterval,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export { handler, query };
|