lib/auth.js - git - Webernet Explorer _X
git 🏠 Home 👤 Sign up Self hosted git, retro shell
★ otgit, code hosting from the year 2000 ★
📄 octuna / lib / auth.js
117 lines · 3.5 KB · Raw
1const crypto = require('crypto');
2const config = require('./config');
3 
4const COOKIE_NAME = 'otgit_sid';
5 
6function cookieSecure() {
7 const setting = config.get('security.cookieSecure');
8 if (setting === 'true') return true;
9 if (setting === 'false') return false;
10 return /^https:/i.test(config.get('site.publicUrl') || '');
11}
12 
13function hashPassword(password, salt) {
14 const N = config.get('auth.scryptN') || 16384;
15 const saltBuf = typeof salt === 'string' ? Buffer.from(salt, 'base64') : salt;
16 return crypto.scryptSync(String(password), saltBuf, 64, { N, r: 8, p: 1 });
17}
18 
19function createPasswordRecord(password) {
20 const salt = crypto.randomBytes(16);
21 const hash = hashPassword(password, salt);
22 return {
23 passwordHash: hash.toString('base64'),
24 passwordSalt: salt.toString('base64')
25 };
26}
27 
28function verifyPassword(candidate, record) {
29 if (!record || !record.passwordHash || !record.passwordSalt) return false;
30 let candidateHash;
31 try {
32 candidateHash = hashPassword(candidate, record.passwordSalt);
33 } catch { return false; }
34 const expected = Buffer.from(record.passwordHash, 'base64');
35 if (candidateHash.length !== expected.length) return false;
36 return crypto.timingSafeEqual(candidateHash, expected);
37}
38 
39function sessionSecret() {
40 const s = config.get('auth.sessionSecret');
41 const buf = Buffer.from(s || '', 'base64');
42 if (buf.length < 16) throw new Error('auth.sessionSecret too short or missing. Run: node setup.js');
43 return buf;
44}
45 
46function signSession(username) {
47 const issued = Date.now().toString(36);
48 const payload = username + '.' + issued;
49 const sig = crypto.createHmac('sha256', sessionSecret()).update(payload).digest('base64url');
50 return payload + '.' + sig;
51}
52 
53function parseCookies(req) {
54 const out = {};
55 const h = req.headers.cookie;
56 if (!h) return out;
57 for (const part of h.split(';')) {
58 const i = part.indexOf('=');
59 if (i < 0) continue;
60 const k = part.slice(0, i).trim();
61 if (k) out[k] = decodeURIComponent(part.slice(i + 1).trim());
62 }
63 return out;
64}
65 
66function getSession(req) {
67 const tok = parseCookies(req)[COOKIE_NAME];
68 if (!tok) return null;
69 const i1 = tok.indexOf('.');
70 const i2 = tok.lastIndexOf('.');
71 if (i1 < 0 || i1 === i2) return null;
72 const username = tok.slice(0, i1);
73 const issued = tok.slice(i1 + 1, i2);
74 const sig = tok.slice(i2 + 1);
75 const expected = crypto.createHmac('sha256', sessionSecret())
76 .update(username + '.' + issued).digest('base64url');
77 const a = Buffer.from(sig);
78 const b = Buffer.from(expected);
79 if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
80 return { username };
81}
82 
83function setSessionCookie(res, username) {
84 const maxAge = (config.get('auth.sessionMaxAgeDays') || 365) * 86400;
85 const parts = [
86 `${COOKIE_NAME}=${signSession(username)}`,
87 'HttpOnly',
88 'SameSite=Strict',
89 'Path=/',
90 `Max-Age=${maxAge}`
91 ];
92 if (cookieSecure()) parts.push('Secure');
93 res.setHeader('Set-Cookie', parts.join('; '));
94}
95 
96function clearSessionCookie(res) {
97 const parts = [`${COOKIE_NAME}=`, 'HttpOnly', 'SameSite=Strict', 'Path=/', 'Max-Age=0'];
98 if (cookieSecure()) parts.push('Secure');
99 res.setHeader('Set-Cookie', parts.join('; '));
100}
101 
102function requireAuth(req, res) {
103 const session = getSession(req);
104 if (!session) {
105 const respond = require('./respond');
106 respond.unauthorized(res);
107 return null;
108 }
109 return session;
110}
111 
112module.exports = {
113 hashPassword, createPasswordRecord, verifyPassword,
114 signSession, getSession, setSessionCookie, clearSessionCookie,
115 requireAuth
116};
117 
Done 🔒 Internet