From da13c770db9f9d229d3bf799b6e091e8c3e49dbd Mon Sep 17 00:00:00 2001 From: Type-Style Date: Thu, 21 Mar 2024 14:44:02 +0100 Subject: [PATCH] 48 move login to seperate controller (#49) * [Task] #43, add label to form * [Task] #48 login controller --- src/app.ts | 2 + src/controller/login.ts | 58 +++++++++++++++++++ src/controller/read.ts | 104 +--------------------------------- src/middleware/logged-in.ts | 13 +++++ src/scripts/token.ts | 49 ++++++++++++++++ src/tests/integration.test.ts | 2 +- src/tests/login.test.ts | 6 +- views/login-form.ejs | 5 +- 8 files changed, 131 insertions(+), 108 deletions(-) create mode 100644 src/controller/login.ts create mode 100644 src/middleware/logged-in.ts create mode 100644 src/scripts/token.ts diff --git a/src/app.ts b/src/app.ts index ce1edff..d7e1084 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,6 +9,7 @@ import cache from './middleware/cache'; import * as error from "./middleware/error"; import writeRouter from '@src/controller/write'; import readRouter from '@src/controller/read'; +import loginRouter from '@src/controller/login'; import path from 'path'; import logger from '@src/scripts/logger'; import { baseRateLimiter } from './middleware/limit'; @@ -57,6 +58,7 @@ app.get('/', (req, res) => { app.use('/write', writeRouter); app.use('/read', readRouter); +app.use('/login', loginRouter); // use httpdocs as static folder app.use('/', express.static(path.join(__dirname, 'httpdocs'), { diff --git a/src/controller/login.ts b/src/controller/login.ts new file mode 100644 index 0000000..7c71236 --- /dev/null +++ b/src/controller/login.ts @@ -0,0 +1,58 @@ +import express, { Request, Response, NextFunction } from 'express'; +import { create as createError } from '@src/middleware/error'; +import logger from '@src/scripts/logger'; +import { crypt, compare } from '@src/scripts/crypt'; +import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; +import { createToken } from '@src/scripts/token'; + +const router = express.Router(); + +router.get("/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { + res.locals.text = "start"; + loginLimiter(req, res, () => { + res.render("login-form"); + }); +}); + +router.post("/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { + logger.log(req.body); + loginLimiter(req, res, async () => { + let validLogin = false; + const user = req.body.user; + const password = req.body.password; + let userFound = false; + if (!user || !password) { + return createError(res, 422, "Body does not contain all expected information", next); + } + + // Loop through all environment variables + for (const key in process.env) { + if (!key.startsWith('USER')) { continue; } + if (key.substring(5) == user) { + userFound = true; + const hash = process.env[key]; + if (hash) { + validLogin = await compare(password, hash); + } + } + } + + // only allow test user in test environment + if (user == "TEST" && validLogin && process.env.NODE_ENV == "production") { + validLogin = false; + } + + if (validLogin) { + const token = createToken(req, res); + res.json({ "token": token }); + } else { + if (!userFound) { + await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks + } + return createError(res, 403, `invalid login credentials`, next); + } + }); +}); + + +export default router; \ No newline at end of file diff --git a/src/controller/read.ts b/src/controller/read.ts index b04893d..bd4b88c 100644 --- a/src/controller/read.ts +++ b/src/controller/read.ts @@ -2,10 +2,7 @@ import express, { Request, Response, NextFunction } from 'express'; import * as file from '@src/scripts/file'; import { create as createError } from '@src/middleware/error'; import { validationResult, query } from 'express-validator'; -import jwt from 'jsonwebtoken'; -import logger from '@src/scripts/logger'; -import { crypt, compare } from '@src/scripts/crypt'; -import { loginSlowDown, loginLimiter, baseSlowDown, baseRateLimiter } from '@src/middleware/limit'; +import { isLoggedIn } from '@src/middleware/logged-in'; const router = express.Router(); @@ -35,105 +32,6 @@ router.get('/', res.json({ entries }); }); -router.get("/login/", baseSlowDown, baseRateLimiter, async function login(req: Request, res: Response) { - res.locals.text = "start"; - loginLimiter(req, res, () => { - res.render("login-form"); - }); -}); - -router.post("/login/", loginSlowDown, async function postLogin(req: Request, res: Response, next: NextFunction) { - logger.log(req.body); - loginLimiter(req, res, async () => { - let validLogin = false; - const user = req.body.user; - const password = req.body.password; - let userFound = false; - if (!user || !password) { - return createError(res, 422, "Body does not contain all expected information", next); - } - - // Loop through all environment variables - for (const key in process.env) { - if (!key.startsWith('USER')) { continue; } - if (key.substring(5) == user) { - userFound = true; - const hash = process.env[key]; - if (hash) { - validLogin = await compare(password, hash); - } - } - } - - // only allow test user in test environment - if (user == "TEST" && validLogin && process.env.NODE_ENV == "production") { - validLogin = false; - } - - if (validLogin) { - const token = createToken(req, res); - res.json({ "token": token }); - } else { - if (!userFound) { - await crypt(password); // If no matching user is found, perform a dummy password comparison to prevent timing attacks - } - return createError(res, 403, `invalid login credentials`, next); - } - }); -}); - -function isLoggedIn(req: Request, res: Response, next: NextFunction) { - const result = validateToken(req); - if (!result.success) { - createError(res, result.status, result.message || "", next) - } else { - next(); - } -} - -function validateToken(req: Request) { - const key = process.env.KEYA; - const header = req.header('Authorization'); - const [type, token] = header ? header.split(' ') : ""; - let payload: string | jwt.JwtPayload = ""; - - // Guard; aka early return for common failures before verifying authorization - if (!key) { return { success: false, status: 500, message: 'Wrong Configuration' }; } - if (!header) { return { success: false, status: 401, message: 'No Authorization header' }; } - if (type !== 'Bearer' || !token) { return { success: false, status: 400, message: 'Invalid Authorization header' }; } - - try { - payload = jwt.verify(token, key); - } catch (err) { - let message = "could not verify"; - if (err instanceof Error) { - message = `${err.name} - ${err.message}`; - } - - return { success: false, status: 403, message: message }; - } - - // don't allow test user in production environment - if (typeof payload == "object" && payload.user == "TEST" && process.env.NODE_ENV == "production") { - return { success: false, status: 403, message: 'test user not allowed on production' }; - } - - return { success: true }; -} -function createToken(req: Request, res: Response) { - const key = process.env.KEYA; - if (!key) { throw new Error('Configuration is wrong'); } - const today = new Date(); - const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); - const payload = { - date: dateString, - user: req.body.user - }; - const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); - res.locals.token = token; - logger.log(JSON.stringify(payload), true); - return token; -} export default router; diff --git a/src/middleware/logged-in.ts b/src/middleware/logged-in.ts new file mode 100644 index 0000000..eddca04 --- /dev/null +++ b/src/middleware/logged-in.ts @@ -0,0 +1,13 @@ +import { Request, Response, NextFunction } from 'express'; +import { validateToken } from '@src/scripts/token'; +import { create as createError } from '@src/middleware/error'; + + +export function isLoggedIn(req: Request, res: Response, next: NextFunction) { + const result = validateToken(req); + if (!result.success) { + createError(res, result.status, result.message || "", next) + } else { + next(); + } +} diff --git a/src/scripts/token.ts b/src/scripts/token.ts new file mode 100644 index 0000000..f26a718 --- /dev/null +++ b/src/scripts/token.ts @@ -0,0 +1,49 @@ +import jwt from 'jsonwebtoken'; +import logger from '@src/scripts/logger'; +import {Request, Response } from 'express'; + + +export function validateToken(req: Request) { + const key = process.env.KEYA; + const header = req.header('Authorization'); + const [type, token] = header ? header.split(' ') : ""; + let payload: string | jwt.JwtPayload = ""; + + // Guard; aka early return for common failures before verifying authorization + if (!key) { return { success: false, status: 500, message: 'Wrong Configuration' }; } + if (!header) { return { success: false, status: 401, message: 'No Authorization header' }; } + if (type !== 'Bearer' || !token) { return { success: false, status: 400, message: 'Invalid Authorization header' }; } + + try { + payload = jwt.verify(token, key); + } catch (err) { + let message = "could not verify"; + if (err instanceof Error) { + message = `${err.name} - ${err.message}`; + } + + return { success: false, status: 403, message: message }; + } + + // don't allow test user in production environment + if (typeof payload == "object" && payload.user == "TEST" && process.env.NODE_ENV == "production") { + return { success: false, status: 403, message: 'test user not allowed on production' }; + } + + return { success: true }; +} + +export function createToken(req: Request, res: Response) { + const key = process.env.KEYA; + if (!key) { throw new Error('Configuration is wrong'); } + const today = new Date(); + const dateString = today.toLocaleDateString("de-DE", { weekday: "short", year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const payload = { + date: dateString, + user: req.body.user + }; + const token = jwt.sign(payload, key, { expiresIn: 60 * 2 }); + res.locals.token = token; + logger.log(JSON.stringify(payload), true); + return token; +} diff --git a/src/tests/integration.test.ts b/src/tests/integration.test.ts index 2e189e0..4fd5f4e 100644 --- a/src/tests/integration.test.ts +++ b/src/tests/integration.test.ts @@ -249,7 +249,7 @@ describe('read and login', () => { }); it('test user can login', async () => { - const response = await axios.post('http://localhost:80/read/login', testData); + const response = await axios.post('http://localhost:80/login', testData); expect(response.status).toBe(200); expect(response.headers['content-type']).toEqual(expect.stringContaining('application/json')); diff --git a/src/tests/login.test.ts b/src/tests/login.test.ts index 3fefe2d..6fbc340 100644 --- a/src/tests/login.test.ts +++ b/src/tests/login.test.ts @@ -16,7 +16,7 @@ describe('Login', () => { let serverStatus = {}; let response = { data: "", status: "" }; try { - response = await axios.get('http://localhost:80/read/login'); + response = await axios.get('http://localhost:80/login'); serverStatus = response.status; } catch (error) { console.error(error); @@ -28,7 +28,7 @@ describe('Login', () => { it('server is blocking requests with large body', async () => { try { - await axios.post('http://localhost:80/read/login', userDataLarge); + await axios.post('http://localhost:80/login', userDataLarge); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { @@ -41,7 +41,7 @@ describe('Login', () => { it('invalid login verification test', async () => { try { - await axios.post('http://localhost:80/read/login', userData); + await axios.post('http://localhost:80/login', userData); } catch (error) { const axiosError = error as AxiosError; if (axiosError.response) { diff --git a/views/login-form.ejs b/views/login-form.ejs index c628aab..802a7c9 100644 --- a/views/login-form.ejs +++ b/views/login-form.ejs @@ -20,7 +20,10 @@ Password: - +

Token: <%= locals.token %>