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