421 lines
10 KiB
JavaScript
421 lines
10 KiB
JavaScript
import * as core from './core.js';
|
|
import * as form from './form.js';
|
|
|
|
let gDatabase = new Database('auth');
|
|
|
|
const kRefreshInterval = 1 * 7 * 24 * 60 * 60 * 1000;
|
|
|
|
/**
|
|
* Makes a Base64 value URL safe
|
|
* @param {string} value
|
|
* @returns TODOC
|
|
*/
|
|
function b64url(value) {
|
|
value = value.replaceAll('+', '-').replaceAll('/', '_');
|
|
let equals = value.indexOf('=');
|
|
|
|
if (equals !== -1) {
|
|
return value.substring(0, equals);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TODOC
|
|
* @param {string} value
|
|
* @returns
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a JSON Web Token
|
|
* @param {object} payload Object: {"name": "username"}
|
|
* @returns the JWT
|
|
*/
|
|
function makeJwt(payload) {
|
|
const ids = ssb.getIdentities(':auth');
|
|
let id;
|
|
|
|
if (ids?.length) {
|
|
id = ids[0];
|
|
} else {
|
|
id = ssb.createIdentity(':auth');
|
|
}
|
|
|
|
const final_payload = b64url(
|
|
base64Encode(
|
|
JSON.stringify(
|
|
Object.assign({}, payload, {
|
|
exp: new Date().valueOf() + kRefreshInterval,
|
|
})
|
|
)
|
|
)
|
|
);
|
|
const jwt = [
|
|
b64url(base64Encode(JSON.stringify({alg: 'HS256', typ: 'JWT'}))),
|
|
final_payload,
|
|
b64url(ssb.hmacsha256sign(final_payload, ':auth', id)),
|
|
].join('.');
|
|
return jwt;
|
|
}
|
|
|
|
/**
|
|
* Validates a JWT ?
|
|
* @param {*} session TODOC
|
|
* @returns
|
|
*/
|
|
function readSession(session) {
|
|
let jwt_parts = session?.split('.');
|
|
|
|
if (jwt_parts?.length === 3) {
|
|
let [header, payload, signature] = jwt_parts;
|
|
header = JSON.parse(utf8Decode(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)) {
|
|
const result = JSON.parse(utf8Decode(base64Decode(unb64url(payload))));
|
|
const 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.');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check the provided password matches the hash
|
|
* @param {string} password
|
|
* @param {string} hash bcrypt hash
|
|
* @returns true if the password matches the hash
|
|
*/
|
|
function verifyPassword(password, hash) {
|
|
return bCrypt.hashpw(password, hash) === hash;
|
|
}
|
|
|
|
/**
|
|
* Hashes a password
|
|
* @param {string} password
|
|
* @returns {string} TODOC
|
|
*/
|
|
function hashPassword(password) {
|
|
let salt = bCrypt.gensalt(12);
|
|
return bCrypt.hashpw(password, salt);
|
|
}
|
|
|
|
/**
|
|
* Check if there is an administrator on the instance
|
|
* @returns TODOC
|
|
*/
|
|
function noAdministrator() {
|
|
return (
|
|
!core.globalSettings ||
|
|
!core.globalSettings.permissions ||
|
|
!Object.keys(core.globalSettings.permissions).some(function (name) {
|
|
return (
|
|
core.globalSettings.permissions[name].indexOf('administration') != -1
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Makes a user an administrator
|
|
* @param {string} name the user's name
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* TODOC
|
|
* @param {*} headers most likely an object
|
|
* @returns
|
|
*/
|
|
function getCookies(headers) {
|
|
let cookies = {};
|
|
|
|
if (headers.cookie) {
|
|
let parts = headers.cookie.split(/,|;/);
|
|
for (let i in parts) {
|
|
let equals = parts[i].indexOf('=');
|
|
let name = parts[i].substring(0, equals).trim();
|
|
let value = parts[i].substring(equals + 1).trim();
|
|
cookies[name] = value;
|
|
}
|
|
}
|
|
|
|
return cookies;
|
|
}
|
|
|
|
/**
|
|
* Validates a username
|
|
* @param {string} name
|
|
* @returns false | boolean[] ?
|
|
*/
|
|
function isNameValid(name) {
|
|
// TODO(tasiaiso): convert this into a regex
|
|
let c = name.charAt(0);
|
|
return (
|
|
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) &&
|
|
name
|
|
.split()
|
|
.map(
|
|
(x) =>
|
|
x >= ('a' && x <= 'z') ||
|
|
x >= ('A' && x <= 'Z') ||
|
|
x >= ('0' && x <= '9')
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Request handler ?
|
|
* @param {*} request TODOC
|
|
* @param {*} response
|
|
* @returns
|
|
*/
|
|
function handler(request, response) {
|
|
// TODO(tasiaiso): split this function
|
|
let session = getCookies(request.headers).session;
|
|
if (request.uri == '/login') {
|
|
let formData = form.decodeForm(request.query);
|
|
if (query(request.headers)?.permissions?.authenticated) {
|
|
if (formData.return) {
|
|
response.writeHead(303, {Location: formData.return});
|
|
} else {
|
|
response.writeHead(303, {
|
|
Location:
|
|
(request.client.tls ? 'https://' : 'http://') +
|
|
request.headers.host +
|
|
'/',
|
|
'Content-Length': '0',
|
|
});
|
|
}
|
|
response.end();
|
|
return;
|
|
}
|
|
|
|
let sessionIsNew = false;
|
|
let loginError;
|
|
|
|
if (request.method == 'POST' || formData.submit) {
|
|
sessionIsNew = true;
|
|
formData = form.decodeForm(utf8Decode(request.body), formData);
|
|
if (formData.submit == 'Login') {
|
|
let account = gDatabase.get('user:' + formData.name);
|
|
account = account ? JSON.parse(account) : account;
|
|
if (formData.register == '1') {
|
|
if (
|
|
!account &&
|
|
isNameValid(formData.name) &&
|
|
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 (formData.change == '1') {
|
|
if (
|
|
account &&
|
|
isNameValid(formData.name) &&
|
|
formData.new_password == formData.confirm &&
|
|
verifyPassword(formData.password, account.password)
|
|
) {
|
|
session = makeJwt({name: formData.name});
|
|
account = {password: hashPassword(formData.new_password)};
|
|
gDatabase.set('user:' + formData.name, JSON.stringify(account));
|
|
} else {
|
|
loginError = 'Error changing password.';
|
|
}
|
|
} 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'});
|
|
}
|
|
}
|
|
|
|
let cookie = `session=${session}; path=/; Max-Age=${kRefreshInterval}; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; HttpOnly`;
|
|
let 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) {
|
|
let html = utf8Decode(data);
|
|
let auth_data = {
|
|
session_is_new: sessionIsNew,
|
|
name: entry?.name,
|
|
error: loginError,
|
|
code_of_conduct: core.globalSettings.code_of_conduct,
|
|
have_administrator: !noAdministrator(),
|
|
};
|
|
html = utf8Encode(
|
|
html.replace('$AUTH_DATA', JSON.stringify(auth_data))
|
|
);
|
|
response.writeHead(200, {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Set-Cookie': cookie,
|
|
'Content-Length': html.length,
|
|
});
|
|
response.end(html);
|
|
})
|
|
.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=/; ${request.client.tls ? 'Secure; ' : ''}SameSite=Strict; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly`,
|
|
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 + '.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a user's permissions based on it's session ?
|
|
* @param {*} session TODOC
|
|
* @returns
|
|
*/
|
|
function getPermissions(session) {
|
|
let permissions;
|
|
let entry = readSession(session);
|
|
if (entry) {
|
|
permissions = getPermissionsForUser(entry.name);
|
|
permissions.authenticated = entry.name !== 'guest';
|
|
}
|
|
return permissions || {};
|
|
}
|
|
|
|
/**
|
|
* Get a user's permissions ?
|
|
* @param {string} userName TODOC
|
|
* @returns
|
|
*/
|
|
function getPermissionsForUser(userName) {
|
|
let permissions = {};
|
|
if (
|
|
core.globalSettings &&
|
|
core.globalSettings.permissions &&
|
|
core.globalSettings.permissions[userName]
|
|
) {
|
|
for (let i in core.globalSettings.permissions[userName]) {
|
|
permissions[core.globalSettings.permissions[userName][i]] = true;
|
|
}
|
|
}
|
|
return permissions;
|
|
}
|
|
|
|
/**
|
|
* TODOC
|
|
* @param {*} headers
|
|
* @returns
|
|
*/
|
|
function query(headers) {
|
|
let session = getCookies(headers).session;
|
|
let entry;
|
|
let autologin = tildefriends.args.autologin;
|
|
if ((entry = autologin ? {name: autologin} : readSession(session))) {
|
|
return {
|
|
session: entry,
|
|
permissions: autologin
|
|
? getPermissionsForUser(autologin)
|
|
: getPermissions(session),
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refreshes a JWT ?
|
|
* @param {*} credentials TODOC
|
|
* @returns
|
|
*/
|
|
function makeRefresh(credentials) {
|
|
if (credentials?.session?.name) {
|
|
return {
|
|
token: makeJwt({name: credentials.session.name}),
|
|
interval: kRefreshInterval,
|
|
};
|
|
}
|
|
}
|
|
|
|
export {handler, query, makeRefresh};
|