UNPKG

huncwot

Version:

A Programming Environment for TypeScript apps built on top of VS Code

201 lines (164 loc) 5.19 kB
// Copyright 2019 Zaiste & contributors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const { ForbiddenError } = require('@casl/ability'); const basicAuth = require('basic-auth'); const argon2 = require('argon2'); const crypto = require('crypto'); const { Unauthorized, Created, Forbidden, InternalServerError } = require('./response.js'); const db = require('./db.js'); const Cookie = require('./cookie.js'); const compare = argon2.verify; const hash = argon2.hash; const fromBase64 = base64 => base64 .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); function auth({ users }) { return async (context, next) => { const credentials = basicAuth(context.request); if (credentials && credentials.name && credentials.pass && check(credentials)) { return next(); } else { return { headers: { 'WWW-Authenticate': 'Basic realm=Authorization Required' }, statusCode: 401, body: '' }; } }; // closure over `users` function check({ name, pass }) { for (let k in users) { if (name === k && pass === users[k]) { return true; } } return false; } } const bearer = (authorization = '') => authorization.startsWith('Bearer ') ? authorization.substring(7) : undefined; const authenticate = action => async request => { const { cookies = {}, headers = {}, params = {} } = request; const token = cookies.__hcsession || bearer(headers.authorization) || params.token; if (!token) return Unauthorized(); const sha256 = crypto.createHash('sha256'); const hashedToken = sha256.update(token).digest('base64'); const [found] = await db`person`.join`session`.on`person.id = session.person_id` .where`token = ${hashedToken}`; if (found) { request.user = found; return action(request); } else { // HTTP 401 Unauthorized is for authentication, not authorization (!) return Unauthorized(); } }; // authorization: a noun, as it leads to creating a process once // both const authorization = ({ using: rules }) => ({ permission = 'read', entity = 'all' }) => action => async request => { const { user } = request; const permissions = rules(user); try { permissions.throwUnlessCan(permission, entity); return action(request); } catch (error) { if (error instanceof ForbiddenError) { return Forbidden(error.message); } else { return InternalServerError(error.message); } } }; class Session { static async create(person_id) { const token = await new Promise((resolve, reject) => { crypto.randomBytes(16, (error, data) => { error ? reject(error) : resolve(fromBase64(data.toString('base64'))); }); }); const sha256 = crypto.createHash('sha256'); const hash = sha256.update(token).digest('base64'); await db`session`.insert({ token: hash, person_id }); return token; } } const register = ({ table = 'person', fields = [] }) => async ({ params }) => { const { password } = params; const hashed_password = await hash(password); let person = {}; for (let field of fields) { person[field] = params[field]; } Object.assign(person, { password: hashed_password }); const transaction = await db.transaction(); try { const [{ id: person_id }] = await db .from(table) .insert(person) .return('id'); // TODO generalize this so people are not force to use `person` table const token = await Session.create(person_id); await transaction.commit(); return Created( { person_id, token }, { 'Set-Cookie': Cookie.create('__hcsession', token, { httpOnly: true, sameSite: true }) } ); } catch (error) { await transaction.rollback(); throw error; } }; const login = ({ finder = () => {} } = {}) => async ({ params }) => { const { password } = params; const [person] = await finder(params); if (person) { const { id: person_id, password: hashed_password } = person; const match = await compare(hashed_password, password); if (match) { const token = await Session.create(person_id); const { password: _, ...rest } = person; // delete is slow, use spread instead return Created(Object.assign({ token }, rest), { 'Set-Cookie': Cookie.create('__hcsession', token, { httpOnly: true, sameSite: true }) }); } else { return Unauthorized(); } } else { return Unauthorized(); } }; module.exports = { auth, authenticate, authorization, hash, compare, Session, register, login };