Skip to content

Commit

Permalink
48 move login to seperate controller (#49)
Browse files Browse the repository at this point in the history
* [Task] #43, add label to form

* [Task] #48 login controller
  • Loading branch information
Type-Style authored Mar 21, 2024
1 parent 45c3a89 commit da13c77
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 108 deletions.
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'), {
Expand Down
58 changes: 58 additions & 0 deletions src/controller/login.ts
Original file line number Diff line number Diff line change
@@ -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;
104 changes: 1 addition & 103 deletions src/controller/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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;
13 changes: 13 additions & 0 deletions src/middleware/logged-in.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
49 changes: 49 additions & 0 deletions src/scripts/token.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/tests/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
6 changes: 3 additions & 3 deletions src/tests/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion views/login-form.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
Password:
<input name="password" type="password" class="login__input" placeholder="Password">
</label>
<button type="submit">Submit</button>
<label>
Submit:
<button type="submit">Submit</button>
</label>
<textarea name="text"></textarea>
<input type="hidden" name="token" value="<%= locals.token %>">
<p>Token: <%= locals.token %></p>
Expand Down

0 comments on commit da13c77

Please sign in to comment.