feat: Added brute force prevention. Closes #3
This commit is contained in:
53
src/helpers/security.ts
Normal file
53
src/helpers/security.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Job, scheduleJob } from "node-schedule";
|
||||||
|
import usettings from "./usettings";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
interface IAccTimeout {
|
||||||
|
firstAttempt: Date;
|
||||||
|
expire: Job;
|
||||||
|
attempts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecurityHelper {
|
||||||
|
private timeouts = new Map<string, IAccTimeout>();
|
||||||
|
private onTimeout = new Map<string, Job>(); // key: user id, value: unlock date
|
||||||
|
constructor () { }
|
||||||
|
|
||||||
|
addAttempt (userId: Types.ObjectId) {
|
||||||
|
var uid = userId.toString()
|
||||||
|
if (this.timeouts.has(uid)) {
|
||||||
|
var t = this.timeouts.get(uid)
|
||||||
|
t.attempts += 1
|
||||||
|
if (t.attempts > usettings.settings.security.loginTimeout.attempts) {
|
||||||
|
this.onTimeout.set(uid, scheduleJob(new Date(Date.now() + usettings.settings.security.loginTimeout.lockout * 1000), () => {
|
||||||
|
this.onTimeout.get(uid).cancel()
|
||||||
|
this.onTimeout.delete(uid)
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
this.timeouts.set(uid, t)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.timeouts.set(uid, {
|
||||||
|
attempts: 1,
|
||||||
|
firstAttempt: new Date(),
|
||||||
|
expire: scheduleJob(new Date(Date.now() + usettings.settings.security.loginTimeout.time * 1000), () => {
|
||||||
|
this.timeouts.get(uid).expire.cancel()
|
||||||
|
this.timeouts.delete(uid)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check(userId: Types.ObjectId) {
|
||||||
|
var timeout = this.onTimeout.get(userId.toString())
|
||||||
|
if (timeout) {
|
||||||
|
// @ts-ignore
|
||||||
|
return timeout.nextInvocation().toDate().valueOf() - Date.now().valueOf()
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new SecurityHelper()
|
||||||
15
src/index.ts
15
src/index.ts
@@ -10,6 +10,7 @@ import mongoose from "mongoose"
|
|||||||
import User from "./schemas/User";
|
import User from "./schemas/User";
|
||||||
import routes from "./routes/index";
|
import routes from "./routes/index";
|
||||||
import process from "node:process"
|
import process from "node:process"
|
||||||
|
import security from "./helpers/security";
|
||||||
const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa";
|
const connectionString = process.env.ATLAS_URI || "mongodb://mongodb:27017/ipwa";
|
||||||
|
|
||||||
if (!process.env.DOMAIN) {
|
if (!process.env.DOMAIN) {
|
||||||
@@ -55,12 +56,20 @@ app.use(passport.session())
|
|||||||
passport.use("normal",new LocalStrategy(async function verify(uname,pass,done) {
|
passport.use("normal",new LocalStrategy(async function verify(uname,pass,done) {
|
||||||
let query = await User.findOne({uname: uname.toLowerCase()})
|
let query = await User.findOne({uname: uname.toLowerCase()})
|
||||||
if (query) {
|
if (query) {
|
||||||
if (query.locked == true) return done(null, false)
|
if (query.locked == true) return done({type: "locked", message: "Twoje konto jest zablokowane. Skontaktuj się z administratorem."}, false)
|
||||||
|
var timeout = security.check(query._id)
|
||||||
|
if (timeout) {
|
||||||
|
timeout = Math.ceil(timeout / 1000 / 60)
|
||||||
|
return done({type: "timeout", message: `Zbyt wiele nieudanych prób logowania. Odczekaj ${timeout} minut lub skontaktuj się z administratorem.`}, false)
|
||||||
|
}
|
||||||
if (await bcrypt.compare(pass, query.pass)) {
|
if (await bcrypt.compare(pass, query.pass)) {
|
||||||
return done(null, query)
|
return done(null, query)
|
||||||
} else done(null, false)
|
} else {
|
||||||
|
security.addAttempt(query._id)
|
||||||
|
done({type: "unf"}, false)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
done(null, false)
|
done({type: "unf"}, false)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@@ -6,12 +6,45 @@ import bcrypt from "bcryptjs"
|
|||||||
import cap from "@/helpers/capability";
|
import cap from "@/helpers/capability";
|
||||||
import usettings from "@/helpers/usettings";
|
import usettings from "@/helpers/usettings";
|
||||||
import vapidKeys from "@/vapidKeys";
|
import vapidKeys from "@/vapidKeys";
|
||||||
|
import { IVerifyOptions } from "passport-local";
|
||||||
|
|
||||||
const authRouter = Router()
|
const authRouter = Router()
|
||||||
|
|
||||||
authRouter.post("/login", passport.authenticate('normal'), (req, res) => {
|
authRouter.post("/login", (req, res) => {
|
||||||
if (req.user.admin != null) res.send({status: 200, admin: req.user.admin})
|
passport.authenticate('normal', (err: {type: string, message: string} | null, user?: Express.User | false, options?: IVerifyOptions) => {
|
||||||
else res.send({status: 200})
|
if (user) {
|
||||||
|
req.login(user, (error) => {
|
||||||
|
if (error) {
|
||||||
|
res.status(500).send(error)
|
||||||
|
} else {
|
||||||
|
if (req.user.admin != null) {
|
||||||
|
res.send({status: 200, admin: req.user.admin})
|
||||||
|
} else {
|
||||||
|
res.send({status: 200})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (err) {
|
||||||
|
switch (err.type) {
|
||||||
|
case "unf":
|
||||||
|
res.status(404).send({status: 404, message: "Zła nazwa użytkownika lub hasło."})
|
||||||
|
break;
|
||||||
|
case "timeout":
|
||||||
|
res.status(403).send({status: 403, message: err.message})
|
||||||
|
break;
|
||||||
|
case "locked":
|
||||||
|
res.status(403).send({status: 403, message: err.message})
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
res.status(500).send({status: 500, message: err.message})
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(403).send({status: 403, message: "Brak hasła lub loginu."})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
authRouter.post("/chpass", islogged, async (req,res) => {
|
authRouter.post("/chpass", islogged, async (req,res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user