1 | "use strict";
|
2 |
|
3 | const audit = require('../helper/audit');
|
4 | const failureImpl = require('../restriction/failure');
|
5 | const generatePassword = require('../password/generate');
|
6 | const json = require('body-parser')
|
7 | .json();
|
8 | const recaptchaImpl = require('../restriction/recaptcha');
|
9 |
|
10 | const Auth = require('./Auth');
|
11 | const LocalStrategy = require('passport-local');
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | class EmailAuth extends Auth
|
19 | {
|
20 |
|
21 | |
22 |
|
23 |
|
24 |
|
25 | constructor(options = {})
|
26 | {
|
27 | super('email', options);
|
28 |
|
29 | |
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | this.emailSender = options.emailSender;
|
37 |
|
38 | |
39 |
|
40 |
|
41 |
|
42 |
|
43 | this.crypt = options.crypt;
|
44 |
|
45 | |
46 |
|
47 |
|
48 |
|
49 |
|
50 | this.applicationName = options.applicationName || 'Account';
|
51 |
|
52 | |
53 |
|
54 |
|
55 |
|
56 |
|
57 | this.fromAddress = options.fromAddress || 'no-thanks@reply-factory.com';
|
58 |
|
59 | this.description.usesPassword = true;
|
60 | this.description.tokenExpiryMinutes = options.tokenExpiryMinutes || 10;
|
61 |
|
62 | |
63 |
|
64 |
|
65 | this.tokenExpiryMilliseconds = this.description.tokenExpiryMinutes * 60 * 1000;
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 | this.block = options.block || undefined;
|
73 |
|
74 | |
75 |
|
76 |
|
77 |
|
78 |
|
79 | this.recaptcha = options.recaptcha || undefined;
|
80 |
|
81 |
|
82 | if (this.recaptcha)
|
83 | {
|
84 | if (this.recaptcha.publicKey)
|
85 | {
|
86 | this.description.recaptcha = this.recaptcha.publicKey;
|
87 | }
|
88 | else
|
89 | {
|
90 | this.recaptcha = undefined;
|
91 | }
|
92 | }
|
93 |
|
94 | |
95 |
|
96 |
|
97 | this.allowPasswordSettingDuringRegistration = options.allowPasswordSettingDuringRegistration || false;
|
98 |
|
99 |
|
100 | |
101 |
|
102 |
|
103 |
|
104 | this.tokens = {};
|
105 | }
|
106 |
|
107 | |
108 |
|
109 |
|
110 | async strategyImpl(req, username, password, done)
|
111 | {
|
112 | const tokens = this.tokens;
|
113 |
|
114 |
|
115 | function error(msg, detailed = undefined)
|
116 | {
|
117 | req.audit(audit.LOGIN_FAILURE, msg, detailed);
|
118 | if (typeof msg === 'string')
|
119 | {
|
120 | msg = {
|
121 | error: msg
|
122 | };
|
123 | }
|
124 | done(null, msg);
|
125 | }
|
126 |
|
127 |
|
128 | const now = Date.now();
|
129 |
|
130 | for (let tok in tokens)
|
131 | {
|
132 | if (now >= tokens[tok].expires)
|
133 | {
|
134 | delete tokens[tok];
|
135 | }
|
136 | }
|
137 |
|
138 |
|
139 | if (tokens[username])
|
140 | {
|
141 | if (tokens[username].password === password)
|
142 | {
|
143 | const profile = await this.createProfileFromEmail(username, tokens[username].extra);
|
144 |
|
145 | delete tokens[username];
|
146 |
|
147 | this.handleUserLoginByProfile(username, profile, done, req);
|
148 | }
|
149 | else
|
150 | {
|
151 | error('Temporary Login password did not match.', username);
|
152 | }
|
153 | }
|
154 | else
|
155 | {
|
156 | let user = this.findUser(username);
|
157 |
|
158 | if (user && user.credentials)
|
159 | {
|
160 | for (let credential of user.credentials)
|
161 | {
|
162 | if (credential.type === this.method && credential.value === username && user.password)
|
163 | {
|
164 | this.crypt.verify(password, user.password)
|
165 | .then((verified) =>
|
166 | {
|
167 | if (verified)
|
168 | {
|
169 | done(null, user);
|
170 | }
|
171 | else
|
172 | {
|
173 | error('Email and password combination not found.', username);
|
174 | }
|
175 | }, done);
|
176 |
|
177 | return;
|
178 | }
|
179 | }
|
180 | }
|
181 | error('Email and password combination not found.', username);
|
182 | }
|
183 | }
|
184 |
|
185 | |
186 |
|
187 |
|
188 | async passwordlessImpl(req, res)
|
189 | {
|
190 | const tokens = this.tokens;
|
191 | try
|
192 | {
|
193 | const username = req.body.username;
|
194 |
|
195 | if (!req.body.username || typeof req.body.username !== 'string')
|
196 | {
|
197 | return res.error('Username not specified', audit.LOGIN_FAILURE);
|
198 | }
|
199 | const temporaryPassword = generatePassword();
|
200 | const theme = req.params.theme || 'login';
|
201 |
|
202 | await this.sendTemporaryPassword(theme, username, temporaryPassword, this.description.tokenExpiryMinutes, req.body.loginLinkPrefix);
|
203 | tokens[username] = {
|
204 | password: temporaryPassword,
|
205 | expires: Date.now() + this.tokenExpiryMilliseconds,
|
206 | extra: req.body,
|
207 | };
|
208 | res.success('A login email has been sent to email address.', audit.LOGIN, username);
|
209 | }
|
210 | catch (e)
|
211 | {
|
212 | console.log(e);
|
213 | res.error('Error sending email. Please verify that your address is correct and is valid.', audit.LOGIN_FAILURE.e);
|
214 | }
|
215 | }
|
216 |
|
217 | |
218 |
|
219 |
|
220 | install(app, prefix, passport)
|
221 | {
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | const limit = this.block ? failureImpl(this.block) : recaptchaImpl(this.recaptcha);
|
227 |
|
228 | passport.use(new LocalStrategy({
|
229 | passReqToCallback: true,
|
230 | }, this.strategyImpl.bind(this)));
|
231 |
|
232 | app.all([`${prefix}/login.json`,
|
233 | `${prefix}/verify.json`
|
234 | ],
|
235 | json,
|
236 | limit,
|
237 | passport.authenticate('local', this.authenticateOptions),
|
238 | this.loggedIn());
|
239 |
|
240 |
|
241 |
|
242 | if (this.emailSender)
|
243 | {
|
244 | app.all(`${prefix}/:theme.json`, json, limit, this.passwordlessImpl.bind(this));
|
245 | }
|
246 | }
|
247 |
|
248 | |
249 |
|
250 |
|
251 | async createProfileFromEmail(id, extra = {})
|
252 | {
|
253 | let displayName = extra.displayName || id.substr(0, id.indexOf('@'));
|
254 | let user = {
|
255 | id,
|
256 | displayName
|
257 | };
|
258 |
|
259 | if (extra.password && this.crypt)
|
260 | {
|
261 | user.password = await this.crypt.hash(extra.password);
|
262 | }
|
263 |
|
264 | return user;
|
265 | }
|
266 |
|
267 |
|
268 | |
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | createUserFromProfile(profile)
|
275 | {
|
276 |
|
277 | let user = super.createUserFromProfile(profile);
|
278 |
|
279 | if (profile.password && this.allowPasswordSettingDuringRegistration)
|
280 | {
|
281 | user.password = profile.password;
|
282 | }
|
283 |
|
284 | return user;
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 | sendTemporaryPassword(theme, to, password, expireMinutes, loginLinkPrefix)
|
299 | {
|
300 | let subject = `${this.applicationName}`;
|
301 |
|
302 | switch (theme)
|
303 | {
|
304 | case 'register':
|
305 | subject += ' Registration';
|
306 | break;
|
307 | case 'recover':
|
308 | subject += ' Account Recovery';
|
309 | break;
|
310 | default:
|
311 | subject += ' Login';
|
312 | break;
|
313 | }
|
314 |
|
315 | let message = '';
|
316 |
|
317 | message += `<p>You have received this email because someone requested access to the ${this.applicationName} using this email address.</p>\n`;
|
318 |
|
319 | if (theme === 'register' && loginLinkPrefix)
|
320 | {
|
321 | message += `<p>Use the following link to verify this email address and complete your registration: <a href="${loginLinkPrefix}${password}">verify</a>.</p>\n`;
|
322 | }
|
323 | else if (theme === 'recover' && loginLinkPrefix)
|
324 | {
|
325 | message += `<p>Use the following link to log into the system to change your password: <a href="${loginLinkPrefix}${password}">login</a>.</p>\n`;
|
326 | }
|
327 | else
|
328 | if (loginLinkPrefix)
|
329 | {
|
330 | message += `<p>Use the following link to log into the system: <a href="${loginLinkPrefix}${password}">login</a>.</p>\n`;
|
331 | message += `<p>Alternatively, you can use the following password: ${password}</p>\n`;
|
332 | message += `<p>Note: this link and password will expire within ${expireMinutes} minutes of request time.</p>\n`;
|
333 | }
|
334 | else
|
335 | {
|
336 | message += `<p>Use the following password to log into the system: ${password}</p>\n`;
|
337 | message += `<p>Note: this password will expire within ${expireMinutes} minutes of request time.</p>\n`;
|
338 | }
|
339 | message += `<p>If this is not you, please contact system administrators.</p>\n`;
|
340 | message += `<p>Kind Regards,</p>\n`;
|
341 | message += `<p>${this.applicationName} Team</p>\n`;
|
342 | message += `<p></p>\n`;
|
343 | message += `<p>WARNING: This is an automaticly generated email. Do not reploy to it.</p>\n`;
|
344 |
|
345 | return this.emailSender.send(to, this.fromAddress, subject, message);
|
346 | }
|
347 | }
|
348 |
|
349 | module.exports = EmailAuth;
|