diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..f86e8ea --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/*.html diff --git a/.travis.yml b/.travis.yml index cfed9b1..8d87d9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,9 @@ cache: - "node_modules" script: - npm test - - npm run staging + - if [ "$TRAVIS_BRANCH" == "master" ] || [ "$TRAVIS_BRANCH" == "development" ] ; then + npm run staging; + fi after_success: npm run coverage deploy: provider: heroku diff --git a/README.md b/README.md index 71014cb..4239674 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Authentication - Jobhub +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/f193348b2004466ba69f53bec6f9de9a)](https://www.codacy.com/app/alexH2456/authentication?utm_source=github.com&utm_medium=referral&utm_content=scrum-gang/authentication&utm_campaign=Badge_Grade) [![Build Status](https://travis-ci.com/scrum-gang/authentication.svg?branch=master)](https://travis-ci.com/scrum-gang/authentication) [![Coverage Status](https://coveralls.io/repos/github/scrum-gang/authentication/badge.svg?branch=master)](https://coveralls.io/github/scrum-gang/authentication?branch=master) diff --git a/doc/login.md b/doc/login.md index c5acdb1..2c7f5df 100644 --- a/doc/login.md +++ b/doc/login.md @@ -29,6 +29,16 @@ Returns session token for existing User on succesful login. ```json { + "user": { + "_id": "[user ID]", + "email": "[email]", + "password": "[hashed password]", + "type": "[user type]", + "verified": true, + "created_at": "[account creation date]", + "updated_at": "[last account update]", + "__v": 0 + }, "iat": "[Token issued at]", "exp": "[Token expiry]", "token": "[JWT token]" diff --git a/doc/selfGet.md b/doc/selfGet.md index 89b5c71..72fe92b 100644 --- a/doc/selfGet.md +++ b/doc/selfGet.md @@ -25,7 +25,10 @@ Returns corresponding user given JWT token. "email": "[User email]", "password": "[User password]", "type": "[User type]", - "verified": true + "verified": true, + "created_at": "[account creation date]", + "updated_at": "[last account update]", + "__v": 0 } ``` diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..022a60d Binary files /dev/null and b/favicon.ico differ diff --git a/package.json b/package.json index 83aac79..ff7d44b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "node src/index.js", "dev": "nodemon src/index.js", "test": "npm run lint && npm run mocha", - "lint": "eslint src/** test/**", + "lint": "eslint .", "mocha": "cross-env NODE_ENV=test nyc --reporter=text mocha --recursive --timeout 10000 --exit", "staging": "cross-env NODE_ENV=staging-test mocha --recursive --timeout 10000 --exit", "coverage": "nyc report --reporter=text-lcov | coveralls" diff --git a/src/config.js b/src/config.js index 117d418..1d58f3f 100644 --- a/src/config.js +++ b/src/config.js @@ -2,6 +2,7 @@ module.exports = { ENV: process.env.NODE_ENV || "development", PORT: process.env.PORT || 3000, URL: process.env.BASE_URL || "http://localhost:3000", + FRONTEND_URL: process.env.FRONTEND_URL || "https://jobhub.netlify.com", MONGODB_URI: process.env.MONGODB_URI, MONGODB_URI_STAGING: process.env.MONGODB_URI_STAGING, JWT_SECRET: process.env.JWT_SECRET || "secret", diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..3e2dc3e --- /dev/null +++ b/src/index.html @@ -0,0 +1,20 @@ + + + + JobHub Authentication + + + +

👮 Welcome to the Jo🅱️hu🅱️ Authentication Microservice 👮

+

Please consult our repo for endpoint information.

+ + diff --git a/src/routes/users.js b/src/routes/users.js index b5f5bec..8104259 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -7,6 +7,8 @@ const InvalidToken = require("../models/InvalidToken"); const auth = require("../auth"); const config = require("../config"); const mail = require("nodemailer"); +const fs = require("fs"); +const path = require("path"); const { google } = require("googleapis"); const OAuth2 = google.auth.OAuth2; @@ -19,6 +21,17 @@ oauth2Client.setCredentials({ refresh_token: config.REFRESH_TOKEN }); +const expire = (config.ENV == "production") ? "15m" : "60m"; + +var loginAttemptCtr=0; +var loginAttempts = new Object(); +var requestCtr = new Object(); +var ipDictLogAtt = new Object(); +var ipDictReqAtt = new Object(); +var timer; +var ip; + + module.exports = server => { function validateEmail(email) { @@ -26,6 +39,23 @@ module.exports = server => { return re.test(email); } + function newRequest(ip){ + if (config.ENV == "production") { + if (requestCtr[ip]==undefined){ + requestCtr[ip]=1; + } else { requestCtr[ip]= requestCtr[ip]+1;} + timer = setTimeout(function(){requestCtr[ip]=requestCtr[ip]-1;}, 60000); + + if (ipDictReqAtt[ip]==1){ + throw "Too many requests, ip timed out."; + } + else if(requestCtr[ip]>=20){ + ipDictReqAtt[ip]=1; + timer = setTimeout(function(){ipDictReqAtt[ip]=0;}, 300000); + } + } + } + var isRevokedCallback = async function (req, payload, done) { if (payload == null) { return done(new errors.UnauthorizedError("Invalid token"), false); @@ -60,16 +90,47 @@ module.exports = server => { } server.get("/", async (req, res, next) => { - var body = "👮 Welcome to the Jobhub Authentication Microservice! 👮"; - res.writeHead(200, { - "Content-Length": Buffer.byteLength(body), - "Content-Type": "text/html" + const rootDir = path.resolve(__dirname, ".."); + const indexPath = path.join(rootDir, "index.html"); + + fs.readFile(indexPath, function (err, file) { + res.writeHead(200, { + "Content-Length": Buffer.byteLength(file), + "Content-Type": "text/html" + }); + res.write(file); + res.end(); + }); + }); + + server.get("/favicon.ico", async (req, res, next) => { + const rootDir = path.resolve(__dirname, "..", ".."); + const favPath = path.join(rootDir, "favicon.ico"); + const stats = await fs.statSync(favPath); + + fs.readFile(favPath, function (err, file) { + if (err) { + res.send(500); + next(); + } + res.writeHead(200, { + "Content-Length": stats.size, + "Content-Type": "image/ico" + }); + res.write(file); + res.end(); }); - res.write(body); - res.end(); }); server.post("/signup", async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + if (typeof req.body === "undefined") { return next( new errors.MissingParameterError( @@ -130,6 +191,8 @@ module.exports = server => { verified: false }); + loginAttempts[email]=0; + bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(user.password, salt, async (err, hash) => { // hash passw @@ -149,7 +212,7 @@ module.exports = server => { function sendEmail (host, user) { const token = jwt.sign({ id: user.id, email: user.email, type: user.type }, config.JWT_SECRET, { - expiresIn: "15m" + expiresIn: expire }); if (config.ENV != "test" && config.ENV != "staging-test") { @@ -198,56 +261,85 @@ module.exports = server => { } } - server.post( - "/resend", - async (req, res, next) => { - const { email } = req.body; - const user = await User.findOne({ email }); - - if (user === null) { - return next(new errors.BadRequestError("No user with given email")); - } else { - if (user.verified) { - return next(new errors.BadRequestError("User is already verified.")); - } - const host = req.header("Host"); - sendEmail(host, user); - res.send(200); + server.post("/resend", async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + + const { email } = req.body; + const user = await User.findOne({ email }); + + + if (user === null) { + return next(new errors.BadRequestError("No user with given email")); + } else { + if (user.verified) { + return next(new errors.BadRequestError("User is already verified.")); } + const host = req.header("Host"); + sendEmail(host, user); + res.send(200); } - ); + }); // auth user - server.post( - "/login", - async (req, res, next) => { - const { email, password } = req.body; + server.post("/login", async (req, res, next) => { + const { email, password } = req.body; + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } - try { - const user = await auth.authenticate(email, password); - const token = jwt.sign( - { id: user.id, email: user.email, type: user.type }, - config.JWT_SECRET, - { - expiresIn: "15m" - } - ); + try { + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + if (ipDictLogAtt[ip]==1){ + throw "Too many login attempts, please try again later."; + } + else if(loginAttempts[email]>=10){ + ipDictLogAtt[ip]=1; + timer = setTimeout(function(){ipDictLogAtt[ip]=0;}, 300000); + } + const user = await auth.authenticate(email, password); + const token = jwt.sign( + { id: user.id, email: user.email, type: user.type }, + config.JWT_SECRET, + { + expiresIn: expire + } + ); - const { iat, exp } = jwt.decode(token); - const payload = Object.assign({}, { user }, { iat, exp, token }); - res.send(payload); + const { iat, exp } = jwt.decode(token); + const payload = Object.assign({}, { user }, { iat, exp, token }); + res.send(payload); + + next(); + } catch (err) { + loginAttempts[email]=loginAttempts[email]+1; + timer = setTimeout(function(){loginAttempts[email]=loginAttempts[email]-1;}, 300000); + return next(new errors.UnauthorizedError(err)); - next(); - } catch (err) { - return next(new errors.UnauthorizedError(err)); - } } - ); + }); server.get( "/users", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { if (moderator(req)) { const users = await User.find({}); @@ -265,6 +357,14 @@ module.exports = server => { "/users/:id", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { if (moderator(req)) { const users = await User.findById(req.params.id); @@ -290,6 +390,14 @@ module.exports = server => { "/users/:id", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + if (!req.is("application/json")) { return next(new errors.InvalidContentError("Expects 'application/json")); } @@ -345,6 +453,14 @@ module.exports = server => { "/users/:id", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { if (moderator(req)) { const user = await User.findOneAndRemove({ _id: req.params.id }); @@ -370,6 +486,14 @@ module.exports = server => { "/users/self", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { const bearer = req.header("Authorization"); const token = bearer.split(" ")[1]; @@ -387,10 +511,48 @@ module.exports = server => { } ); + server.get("/verify/:header/:payload/:signature", async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + + try { + const token = + req.params.header + + "." + + req.params.payload + + "." + + req.params.signature; + const { email, iat, exp } = jwt.decode(token); + + const user = await User.findOneAndUpdate( + { email: email }, + { verified: true } + ); + + res.send({ iat, exp, token }, 200); + next(); + } catch (err) { + return next(new errors.UnauthorizedError("Invalid token.")); + } + }); + server.put( "/users/self", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + if (!req.is("application/json")) { return next(new errors.InvalidContentError("Expects 'application/json")); } @@ -445,6 +607,14 @@ module.exports = server => { "/users/self", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async(req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + const bearer = req.header("Authorization"); const token = bearer.split(" ")[1]; const payload = jwt.decode(token); @@ -458,6 +628,14 @@ module.exports = server => { server.get( "/verify", async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { const token = req.query.header + @@ -472,8 +650,8 @@ module.exports = server => { { verified: true } ); - res.send({ iat, exp, token }, 200); - next(); + // res.send({ iat, exp, token }, 200); + res.redirect(config.FRONTEND_URL + "/login", next); } catch (err) { return next(new errors.UnauthorizedError("Invalid token.")); } @@ -484,6 +662,14 @@ module.exports = server => { "/logout", rjwt({ secret: config.JWT_SECRET, isRevoked: isRevokedCallback }), async (req, res, next) => { + + ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; + try{ + newRequest(ip); + } catch (err) { + return next(new errors.UnauthorizedError(err)); + } + try { const token = req.header("Authorization").split(" ")[1]; const { email, iat, exp } = jwt.decode(token);