Skip to content

Commit 37f99be

Browse files
DiverVMtiblu
authored andcommitted
Feature: Set the password reset time to 1 hour [#68] (#459)
* New Crowdin updates (#457) * New translations source.json (French) * New translations source.json (Spanish) * New translations source.json (Arabic) * New translations source.json (Belarusian) * New translations source.json (Bulgarian) * New translations source.json (Czech) * New translations source.json (German) * New translations source.json (Finnish) * New translations source.json (Japanese) * New translations source.json (Georgian) * New translations source.json (Lithuanian) * New translations source.json (Macedonian) * New translations source.json (Dutch) * New translations source.json (Polish) * New translations source.json (Russian) * New translations source.json (Slovak) * New translations source.json (Albanian) * New translations source.json (Ukrainian) * New translations source.json (Chinese Simplified) * New translations source.json (Portuguese, Brazilian) * New translations source.json (Indonesian) * New translations source.json (Estonian) * New translations source.json (Latvian) * New translations source.json (Hindi) * New translations source.json (English, United Kingdom) * New translations source.json (Burmese) * refactor redis, add cache * add cache token for reset password link --------- Co-authored-by: tiblu <[email protected]>
1 parent 0a787b6 commit 37f99be

File tree

6 files changed

+340
-246
lines changed

6 files changed

+340
-246
lines changed

app.js

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const config = require('config');
44
const express = require('express');
55
const session = require('express-session');
6-
const RedisStoreSession = require("connect-redis").default
76
const path = require('path');
87
const cookieParser = require('cookie-parser');
98
const bodyParser = require('body-parser');
@@ -35,39 +34,12 @@ const StreamUpload = require('stream_upload');
3534
const notifications = require('./libs/notifications');
3635
const SlowDown = require('express-slow-down');
3736
const rateLimit = require('express-rate-limit')
38-
const { createClient } = require('redis');
3937
const which = require('which');
4038

41-
let rateLimitStore, speedLimitStore;
42-
if (config.rateLimit && config.rateLimit.storageType === 'redis') {
43-
const { RedisStore } = require('rate-limit-redis');
44-
const redisUrl = config.rateLimit.client?.url;
45-
const redisOptions = config.rateLimit.client?.options;
46-
const redisConf = Object.assign({ url: process.env.REDIS_URL || redisUrl }, redisOptions);
47-
const client = createClient(redisConf);
48-
49-
client.on('error', err => logger.error('Redis Client Error', err))
50-
client.on('end', () => {
51-
logger.log('Redis connection ended');
52-
});
53-
client.connect();
54-
rateLimitStore = new RedisStore({
55-
client,
56-
prefix: 'rl',
57-
sendCommand: (...args) => client.sendCommand(args)
58-
});
39+
const app = express();
40+
app.set('redis', require('./libs/redis')(app));
5941

60-
speedLimitStore = new RedisStore({
61-
client,
62-
prefix: 'sl',
63-
sendCommand: (...args) => client.sendCommand(args)
64-
});
65-
66-
/*Set Redis Session store*/
67-
config.session.store = new RedisStoreSession({
68-
client
69-
});
70-
}
42+
const { rateLimitStore, speedLimitStore } = app.get('redis');
7143

7244
const rateLimiter = function (allowedRequests, blockTime, skipSuccess) {
7345
if (app.get('env') === 'test') {
@@ -108,8 +80,6 @@ const speedLimiter = function (allowedRequests, skipSuccess, blockTime, delay) {
10880
})
10981
};
11082

111-
const app = express();
112-
11383
// Express settings
11484
// TODO: Would be nice if conf had express.settings.* and all from there would be set
11585
if (app.get('env') === 'production' || app.get('env') === 'test') {

libs/redis/cache.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use strict";
2+
3+
module.exports = function (client) {
4+
const setResetPasswordToken = async (key, ttl, data) => {
5+
await client.setEx(key, ttl, JSON.stringify(data));
6+
};
7+
8+
const getResetPasswordToken = async (key) => {
9+
const value = await client.get(key);
10+
return value ? JSON.parse(value) : null;
11+
};
12+
13+
const deleteResetPasswordToken = async (key) => {
14+
await client.del(key);
15+
};
16+
17+
return {
18+
setResetPasswordToken,
19+
getResetPasswordToken,
20+
deleteResetPasswordToken,
21+
};
22+
};

libs/redis/index.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use strict";
2+
3+
const config = require("config");
4+
const { createClient } = require("redis");
5+
const log4js = require("log4js");
6+
7+
module.exports = function (app) {
8+
const logger = log4js.getLogger(app.settings.env);
9+
10+
const redisUrl = config.rateLimit?.client?.url;
11+
const redisOptions = config.rateLimit?.client?.options;
12+
const redisConf = Object.assign(
13+
{ url: process.env.REDIS_URL || redisUrl },
14+
redisOptions
15+
);
16+
const client = createClient(redisConf);
17+
18+
client.on("error", (err) => logger.error("Redis Client Error", err));
19+
client.on("end", () => {
20+
logger.log("Redis connection ended");
21+
});
22+
client.connect();
23+
24+
const {
25+
setResetPasswordToken,
26+
getResetPasswordToken,
27+
deleteResetPasswordToken,
28+
} = require("./cache")(client);
29+
30+
const { rateLimitStore, speedLimitStore } = require("./limitStores")(client);
31+
32+
return {
33+
rateLimitStore,
34+
speedLimitStore,
35+
client,
36+
setResetPasswordToken,
37+
getResetPasswordToken,
38+
deleteResetPasswordToken,
39+
};
40+
};

libs/redis/limitStores.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use strict";
2+
3+
const config = require("config");
4+
const RedisStoreSession = require("connect-redis").default;
5+
const { RedisStore } = require("rate-limit-redis");
6+
7+
module.exports = function (client) {
8+
let rateLimitStore, speedLimitStore;
9+
if (config.rateLimit && config.rateLimit.storageType === "redis") {
10+
rateLimitStore = new RedisStore({
11+
client,
12+
prefix: "rl",
13+
sendCommand: (...args) => client.sendCommand(args),
14+
});
15+
16+
speedLimitStore = new RedisStore({
17+
client,
18+
prefix: "sl",
19+
sendCommand: (...args) => client.sendCommand(args),
20+
});
21+
22+
/*Set Redis Session store*/
23+
config.session.store = new RedisStoreSession({
24+
client,
25+
});
26+
}
27+
28+
return {
29+
rateLimitStore,
30+
speedLimitStore,
31+
};
32+
};

routes/api/auth.js

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ module.exports = function (app) {
2727
const rateLimiter = app.get('rateLimiter');
2828
const expressRateLimitInput = app.get('middleware.expressRateLimitInput');
2929

30+
const {
31+
setResetPasswordToken,
32+
getResetPasswordToken,
33+
deleteResetPasswordToken
34+
} = app.get('redis')
35+
3036
const User = models.User;
3137
const UserConnection = models.UserConnection;
3238
const UserConsent = models.UserConsent;
@@ -36,6 +42,9 @@ module.exports = function (app) {
3642
const COOKIE_NAME_OPENID_AUTH_STATE = 'cos.authStateOpenId';
3743
const COOKIE_NAME_COS_AUTH_STATE = 'cos.authState';
3844

45+
const cachedResetCodeKey = (userId) => `${userId}:resetPasswordToken`;
46+
const CACHED_RESET_TOKEN_TTL = 3600;
47+
3948
/**
4049
* Set state cookie with all in req.query when it does not exist
4150
*
@@ -441,7 +450,6 @@ module.exports = function (app) {
441450
return res.ok();
442451
}));
443452

444-
445453
app.post('/api/auth/password/reset/send', asyncMiddleware(async function (req, res) {
446454
const email = req.body.email;
447455
if (!email || !validator.isEmail(email)) {
@@ -458,11 +466,22 @@ module.exports = function (app) {
458466
return res.ok('Success! Please check your email :email to complete your password recovery.'.replace(':email', email));
459467
}
460468

461-
user.passwordResetCode = true; // Model will generate new code
462-
463-
await user.save({fields: ['passwordResetCode']});
469+
const key = cachedResetCodeKey(user.id)
470+
const cachedResetCode = await getResetPasswordToken(key);
471+
472+
if (!cachedResetCode) {
473+
user.passwordResetCode = true; // Model will generate new code
474+
await user.save({ fields: ["passwordResetCode"] });
475+
await setResetPasswordToken(key, CACHED_RESET_TOKEN_TTL, user.passwordResetCode);
476+
}
477+
478+
const passwordResetCode = cachedResetCode || user.passwordResetCode;
479+
480+
const emailResult = await emailLib.sendPasswordReset(
481+
user.email,
482+
passwordResetCode
483+
);
464484

465-
const emailResult = await emailLib.sendPasswordReset(user.email, user.passwordResetCode);
466485
if (emailResult.done.length === 0 && emailResult.errors.length) {
467486
return res.badRequest(emailResult.errors[0].details)
468487
}
@@ -484,15 +503,25 @@ module.exports = function (app) {
484503
}
485504
});
486505

506+
const cachedResetCode = await getResetPasswordToken(cachedResetCodeKey(user.id));
507+
487508
// !user.passwordResetCode avoids the situation where passwordResetCode has not been sent (null), but user posts null to API
488-
if (!user || !user.passwordResetCode) {
489-
return res.badRequest('Invalid email, password or password reset code.');
509+
if (
510+
!user ||
511+
!user.passwordResetCode ||
512+
!cachedResetCode ||
513+
user.passwordResetCode !== cachedResetCode
514+
) {
515+
return res.badRequest(
516+
"Invalid email, password or password reset code."
517+
);
490518
}
491519

492520
user.password = password; // Hash is created by the model hooks
493-
user.passwordResetCode = true; // Model will generate new code so that old code cannot be used again - https://github.com/citizenos/citizenos-api/issues/68
494521

495-
await user.save({fields: ['password', 'passwordResetCode']});
522+
await user.save({fields: ['password']});
523+
await deleteResetPasswordToken(cachedResetCodeKey(user.id));
524+
496525
//TODO: Logout all existing sessions for the User!
497526
return res.ok();
498527
}));

0 commit comments

Comments
 (0)