UNPKG

5.24 kBJavaScriptView Raw
1// Copyright 2019 Zaiste & contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14const { ForbiddenError } = require('@casl/ability');
15const basicAuth = require('basic-auth');
16const argon2 = require('argon2');
17const crypto = require('crypto');
18
19const {
20 Unauthorized,
21 Created,
22 Forbidden,
23 InternalServerError
24} = require('./response.js');
25const db = require('./db.js');
26const Cookie = require('./cookie.js');
27
28const compare = argon2.verify;
29const hash = argon2.hash;
30
31const fromBase64 = base64 =>
32 base64
33 .replace(/=/g, '')
34 .replace(/\+/g, '-')
35 .replace(/\//g, '_');
36
37function auth({ users }) {
38 return async (context, next) => {
39 const credentials = basicAuth(context.request);
40
41 if (
42 credentials &&
43 credentials.name &&
44 credentials.pass &&
45 check(credentials)
46 ) {
47 return next();
48 } else {
49 return {
50 headers: {
51 'WWW-Authenticate': 'Basic realm=Authorization Required'
52 },
53 statusCode: 401,
54 body: ''
55 };
56 }
57 };
58
59 // closure over `users`
60 function check({ name, pass }) {
61 for (let k in users) {
62 if (name === k && pass == users[k]) {
63 return true;
64 }
65 }
66 return false;
67 }
68}
69
70const bearer = (authorization = '') =>
71 authorization.startsWith('Bearer ') ? authorization.substring(7) : undefined;
72
73const authenticate = action => async request => {
74 const { cookies = {}, headers = {}, params = {} } = request;
75
76 const token =
77 cookies.__hcsession || bearer(headers.authorization) || params.token;
78
79 if (!token) return Unauthorized();
80
81 const sha256 = crypto.createHash('sha256');
82 const hashedToken = sha256.update(token).digest('base64');
83 const [found] = await db`person`.join`session`
84 .on`person.id = session.person_id`.where`token = ${hashedToken}`;
85
86 if (found) {
87 request.user = found;
88
89 return await action(request);
90 } else {
91 // HTTP 401 Unauthorized is for authentication, not authorization (!)
92 return Unauthorized();
93 }
94};
95
96// authorization: a noun, as it leads to creating a process once
97// both
98
99const authorization = ({ using: rules }) => ({
100 permission = 'read',
101 entity = 'all'
102}) => action => async request => {
103 const { user } = request;
104 const permissions = rules(user);
105
106 try {
107 permissions.throwUnlessCan(permission, entity);
108
109 return action(request);
110 } catch (error) {
111 if (error instanceof ForbiddenError) {
112 return Forbidden(error.message);
113 } else {
114 return InternalServerError(error.message);
115 }
116 }
117};
118
119class Session {
120 static async create(person_id) {
121 const token = await new Promise((resolve, reject) => {
122 crypto.randomBytes(16, (error, data) => {
123 error ? reject(error) : resolve(fromBase64(data.toString('base64')));
124 });
125 });
126
127 const sha256 = crypto.createHash('sha256');
128 const hash = sha256.update(token).digest('base64');
129
130 await db`session`.insert({ token: hash, person_id });
131
132 return token;
133 }
134}
135
136const register = ({ table = 'person', fields = [] }) => async ({ params }) => {
137 const { password } = params;
138
139 const hashed_password = await hash(password);
140
141 let person = {};
142 for (let field of fields) {
143 person[field] = params[field];
144 }
145 Object.assign(person, { password: hashed_password });
146
147 const transaction = await db.transaction();
148
149 try {
150 const [{ id: person_id }] = await db
151 .from(table)
152 .insert(person)
153 .return('id');
154
155 // TODO generalize this so people are not force to use `person` table
156 const token = await Session.create(person_id);
157
158 await transaction.commit();
159
160 return Created(
161 { person_id, token },
162 {
163 'Set-Cookie': Cookie.create('__hcsession', token, {
164 httpOnly: true,
165 sameSite: true
166 })
167 }
168 );
169 } catch (error) {
170 await transaction.rollback();
171 throw error;
172 }
173};
174
175const login = ({ finder = () => {} } = {}) => async ({ params }) => {
176 const { password } = params;
177
178 const [person] = await finder(params);
179
180 if (person) {
181 const { id: person_id, password: hashed_password } = person;
182
183 const match = await compare(hashed_password, password);
184
185 if (match) {
186 const token = await Session.create(person_id);
187 const { password: _, ...rest } = person; // delete is slow, use spread instead
188 return Created(Object.assign({ token }, rest), {
189 'Set-Cookie': Cookie.create('__hcsession', token, {
190 httpOnly: true,
191 sameSite: true
192 })
193 });
194 } else {
195 return Unauthorized();
196 }
197 } else {
198 return Unauthorized();
199 }
200};
201
202module.exports = {
203 auth,
204 authenticate,
205 authorization,
206 hash,
207 compare,
208 Session,
209 register,
210 login
211};