-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
134 lines (124 loc) · 3.95 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
const { randomBytes } = require('crypto');
const base32 = require('thirty-two');
const totp = require('totp-generator');
const path = require('path');
const fs = require('fs');
module.exports = {
improve: '@apostrophecms/login',
bundle: {
directory: 'modules',
modules: getBundleModuleNames()
},
i18n: {
aposTotp: {
browser: true
}
},
init (self, { totp }) {
if (!totp.secret) {
self.apos.util.warn('You should provide a secret 10 characters in length in the login module\'s config.');
} else if (totp.secret.length !== 10) {
self.apos.util.warn('Your secret should be exactly 10 characters in length.');
}
},
requirements(self) {
return {
add: {
AposTotp: {
phase: 'afterPasswordVerified',
askForConfirmation: true,
async props(req, user) {
const safe = await self.apos.user.safe.findOne({
_id: user._id
});
if (!safe.totp || !safe.totp.activated) {
const validSecret = self.getSecret();
const hash = randomBytes(validSecret ? 5 : 10).toString('hex');
const token = self.generateToken(hash, validSecret);
const result = await self.apos.user.safe.updateOne({
_id: user._id
}, {
$set: {
totp: {
hash,
activated: false
}
}
});
if (!result.modifiedCount) {
throw self.apos.error('notfound');
}
return {
token,
// Allows multiple identities on the same site to be distinguished
// in a TOTP app
identity: `${user.username}@${self.apos.shortName}`
};
}
return {};
},
async verify(req, data, user) {
const code = self.apos.launder.string(data);
if (!code) {
throw self.apos.error('invalid', req.t('aposTotp:invalidToken'));
}
const safe = await self.apos.user.safe.findOne({
_id: user._id
});
if (!safe.totp) {
throw self.apos.error('invalid', req.t('aposTotp:notConfigured'));
}
const userToken = self.generateToken(safe.totp.hash, self.getSecret());
const totpToken = totp(userToken);
if (totpToken !== code) {
self.logInfo(req, 'totp-invalid-token', {
username: safe.username
});
throw self.apos.error('invalid', req.t('aposTotp:invalidToken'));
}
if (!safe.totp.activated) {
try {
const result = await self.apos.user.safe.updateOne({
_id: user._id
}, {
$set: {
'totp.activated': true
}
});
if (!result.modifiedCount) {
throw self.apos.error('notfound');
}
} catch (err) {
throw self.apos.error('unprocessable', req.t('aposTotp:updateError'));
}
}
self.logInfo(req, 'totp-complete', {
username: safe.username
});
}
}
}
};
},
methods(self) {
return {
generateToken (hash, secret) {
const formattedSecret = secret
? secret.substring(0, 10)
: '';
return base32.encode(hash + formattedSecret).toString();
},
getSecret () {
const { secret } = self.options.totp;
return secret;
}
};
}
};
function getBundleModuleNames() {
const source = path.join(__dirname, './modules/@apostrophecms');
return fs
.readdirSync(source, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => `@apostrophecms/${dirent.name}`);
}