reset totp #14

Merged
jkeffects merged 1 commit from #12-lost-totp into main 2024-11-23 11:11:56 +00:00
14 changed files with 354 additions and 83 deletions
Showing only changes of commit fa1eb6a5f0 - Show all commits

View file

@ -0,0 +1,10 @@
export interface CreateResetCommand {
mail: string;
username: string;
secret: string;
}
export interface DeleteResetCommand {
token: string;
mail: string;
}

View file

@ -0,0 +1,54 @@
import { dataSource } from "../data-source";
import { reset } from "../entity/reset";
import InternalException from "../exceptions/internalException";
import { StringHelper } from "../helpers/stringHelper";
import { CreateResetCommand, DeleteResetCommand } from "./resetCommand";
export default abstract class ResetCommandHandler {
/**
* @description create user
* @param CreateResetCommand
* @returns {Promise<string>}
*/
static async create(createReset: CreateResetCommand): Promise<string> {
const token = StringHelper.random(32);
return await dataSource
.createQueryBuilder()
.insert()
.into(reset)
.values({
token: token,
mail: createReset.mail,
username: createReset.username,
secret: createReset.secret,
})
.orUpdate(["token", "secret"], ["mail"])
.execute()
.then((result) => {
return token;
})
.catch((err) => {
throw new InternalException("Failed saving reset", err);
});
}
/**
* @description delete reset by mail and token
* @param DeleteRefreshCommand
* @returns {Promise<any>}
*/
static async deleteByTokenAndMail(deleteReset: DeleteResetCommand): Promise<any> {
return await dataSource
.createQueryBuilder()
.delete()
.from(reset)
.where("reset.token = :token", { token: deleteReset.token })
.andWhere("reset.mail = :mail", { mail: deleteReset.mail })
.execute()
.then((res) => {})
.catch((err) => {
throw new InternalException("failed reset removal", err);
});
}
}

View file

@ -15,6 +15,11 @@ export interface UpdateUserCommand {
lastname: string;
}
export interface UpdateUserSecretCommand {
id: number;
secret: string;
}
export interface TransferUserOwnerCommand {
fromId: number;
toId: number;

View file

@ -8,6 +8,7 @@ import {
TransferUserOwnerCommand,
UpdateUserCommand,
UpdateUserRolesCommand,
UpdateUserSecretCommand,
} from "./userCommand";
import UserService from "../service/userService";
@ -62,6 +63,26 @@ export default abstract class UserCommandHandler {
});
}
/**
* @description update user
* @param UpdateUserSecretCommand
* @returns {Promise<void>}
*/
static async updateSecret(updateUser: UpdateUserSecretCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(user)
.set({
secret: updateUser.secret,
})
.where("id = :id", { id: updateUser.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating user secret", err);
});
}
/**
* @description update user roles
* @param UpdateUserRolesCommand

View file

@ -22,7 +22,7 @@ export async function login(req: Request, res: Response): Promise<any> {
let username = req.body.username;
let totp = req.body.totp;
let { id, secret, mail, firstname, lastname, isOwner } = await UserService.getByUsername(username);
let { id, secret } = await UserService.getByUsername(username);
let valid = speakeasy.totp.verify({
secret: secret,
@ -35,39 +35,12 @@ export async function login(req: Request, res: Response): Promise<any> {
throw new UnauthorizedRequestException("Token not valid or expired");
}
let userPermissions = await UserPermissionService.getByUser(id);
let userPermissionStrings = userPermissions.map((e) => e.permission);
let userRoles = await UserService.getAssignedRolesByUserId(id);
let rolePermissions = userRoles.length != 0 ? await RolePermissionService.getByRoles(userRoles.map((e) => e.id)) : [];
let rolePermissionStrings = rolePermissions.map((e) => e.permission);
let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]);
let jwtData: JWTToken = {
userId: id,
mail: mail,
username: username,
firstname: firstname,
lastname: lastname,
isOwner: isOwner,
permissions: permissionObject,
};
let accessToken: string;
let refreshToken: string;
JWTHelper.create(jwtData)
.then((result) => {
accessToken = result;
})
.catch((err) => {
console.log(err);
throw new InternalException("Failed accessToken creation", err);
});
let accessToken = await JWTHelper.buildToken(id);
let refreshCommand: CreateRefreshCommand = {
userId: id,
};
refreshToken = await RefreshCommandHandler.create(refreshCommand);
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
res.json({
accessToken,
@ -106,40 +79,15 @@ export async function refresh(req: Request, res: Response): Promise<any> {
throw new UnauthorizedRequestException("user not identified with token and refresh");
}
let { id, username, mail, firstname, lastname, isOwner } = await UserService.getById(tokenUserId);
let permissions = await UserPermissionService.getByUser(id);
let permissionStrings = permissions.map((e) => e.permission);
let permissionObject = PermissionHelper.convertToObject(permissionStrings);
let jwtData: JWTToken = {
userId: id,
mail: mail,
username: username,
firstname: firstname,
lastname: lastname,
isOwner: isOwner,
permissions: permissionObject,
};
let accessToken: string;
let refreshToken: string;
JWTHelper.create(jwtData)
.then((result) => {
accessToken = result;
})
.catch((err) => {
throw new InternalException("Failed accessToken creation", err);
});
let accessToken = await JWTHelper.buildToken(tokenUserId);
let refreshCommand: CreateRefreshCommand = {
userId: id,
userId: tokenUserId,
};
refreshToken = await RefreshCommandHandler.create(refreshCommand);
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
let removeToken: DeleteRefreshCommand = {
userId: id,
userId: tokenUserId,
token: refresh,
};
await RefreshCommandHandler.deleteByToken(removeToken);

View file

@ -128,33 +128,12 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool
};
let id = await UserCommandHandler.create(createUser);
let jwtData: JWTToken = {
userId: id,
mail: mail,
username: username,
firstname: firstname,
lastname: lastname,
isOwner: grantAdmin,
permissions: {
...(grantAdmin ? { admin: true } : {}),
},
};
let accessToken: string;
let refreshToken: string;
JWTHelper.create(jwtData)
.then((result) => {
accessToken = result;
})
.catch((err) => {
throw new InternalException("Failed accessToken creation", err);
});
let accessToken = await JWTHelper.buildToken(id);
let refreshCommand: CreateRefreshCommand = {
userId: id,
};
refreshToken = await RefreshCommandHandler.create(refreshCommand);
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
let deleteInvite: DeleteInviteCommand = {
mail: mail,

View file

@ -0,0 +1,129 @@
import { Request, Response } from "express";
import { JWTHelper } from "../helpers/jwtHelper";
import { JWTToken } from "../type/jwtTypes";
import InternalException from "../exceptions/internalException";
import RefreshCommandHandler from "../command/refreshCommandHandler";
import { CreateRefreshCommand } from "../command/refreshCommand";
import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import QRCode from "qrcode";
import { CreateResetCommand, DeleteResetCommand } from "../command/resetCommand";
import ResetCommandHandler from "../command/resetCommandHandler";
import MailHelper from "../helpers/mailHelper";
import ResetService from "../service/resetService";
import UserService from "../service/userService";
import { CLUB_NAME } from "../env.defaults";
import PermissionHelper from "../helpers/permissionHelper";
import RolePermissionService from "../service/rolePermissionService";
import UserPermissionService from "../service/userPermissionService";
import { UpdateUserSecretCommand } from "../command/userCommand";
import UserCommandHandler from "../command/userCommandHandler";
/**
* @description request totp reset
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function startReset(req: Request, res: Response): Promise<any> {
let origin = req.headers.origin;
let username = req.body.username;
let { mail } = await UserService.getByUsername(username);
var secret = speakeasy.generateSecret({ length: 20, name: `Mitgliederverwaltung ${CLUB_NAME}` });
let createReset: CreateResetCommand = {
username: username,
mail: mail,
secret: secret.base32,
};
let token = await ResetCommandHandler.create(createReset);
// sendmail
let mailhelper = new MailHelper();
await mailhelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`
);
res.sendStatus(204);
}
/**
* @description verify reset link
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function verifyReset(req: Request, res: Response): Promise<any> {
let mail = req.body.mail;
let token = req.body.token;
let { secret } = await ResetService.getByMailAndToken(mail, token);
const url = `otpauth://totp/Mitgliederverwaltung ${CLUB_NAME}?secret=${secret}`;
QRCode.toDataURL(url)
.then((result) => {
res.json({
dataUrl: result,
otp: secret,
});
})
.catch((err) => {
throw new InternalException("QRCode not created", err);
});
}
/**
* @description finishReset
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function finishReset(req: Request, res: Response): Promise<any> {
let mail = req.body.mail;
let token = req.body.token;
let totp = req.body.totp;
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
let valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: totp,
window: 2,
});
if (!valid) {
throw new UnauthorizedRequestException("Token not valid or expired");
}
let { id } = await UserService.getByUsername(username);
let updateUserSecret: UpdateUserSecretCommand = {
id,
secret,
};
await UserCommandHandler.updateSecret(updateUserSecret);
let accessToken = await JWTHelper.buildToken(id);
let refreshCommand: CreateRefreshCommand = {
userId: id,
};
let refreshToken = await RefreshCommandHandler.create(refreshCommand);
let deleteReset: DeleteResetCommand = {
mail: mail,
token: token,
};
await ResetCommandHandler.deleteByTokenAndMail(deleteReset);
res.json({
accessToken,
refreshToken,
});
}

View file

@ -40,6 +40,8 @@ import { Protocol1729347911107 } from "./migrations/1729347911107-protocol";
import { calendar } from "./entity/calendar";
import { calendarType } from "./entity/calendarType";
import { Calendar1729947763295 } from "./migrations/1729947763295-calendar";
import { reset } from "./entity/reset";
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
const dataSource = new DataSource({
type: DB_TYPE as any,
@ -55,6 +57,7 @@ const dataSource = new DataSource({
user,
refresh,
invite,
reset,
userPermission,
role,
rolePermission,
@ -90,6 +93,7 @@ const dataSource = new DataSource({
Ownership1728313041449,
Protocol1729347911107,
Calendar1729947763295,
ResetToken1732358596823,
],
migrationsRun: true,
migrationsTransactionMode: "each",

16
src/entity/reset.ts Normal file
View file

@ -0,0 +1,16 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity()
export class reset {
@PrimaryColumn({ type: "varchar", length: 255 })
mail: string;
@Column({ type: "varchar", length: 255 })
token: string;
@Column({ type: "varchar", length: 255 })
username: string;
@Column({ type: "varchar", length: 255 })
secret: string;
}

View file

@ -1,6 +1,11 @@
import jwt from "jsonwebtoken";
import { JWTData } from "../type/jwtTypes";
import { JWTData, JWTToken } from "../type/jwtTypes";
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
import InternalException from "../exceptions/internalException";
import RolePermissionService from "../service/rolePermissionService";
import UserPermissionService from "../service/userPermissionService";
import UserService from "../service/userService";
import PermissionHelper from "./permissionHelper";
export abstract class JWTHelper {
static validate(token: string): Promise<string | jwt.JwtPayload> {
@ -38,4 +43,33 @@ export abstract class JWTHelper {
}
});
}
static async buildToken(id: number): Promise<string> {
let { firstname, lastname, mail, username, isOwner } = await UserService.getById(id);
let userPermissions = await UserPermissionService.getByUser(id);
let userPermissionStrings = userPermissions.map((e) => e.permission);
let userRoles = await UserService.getAssignedRolesByUserId(id);
let rolePermissions =
userRoles.length != 0 ? await RolePermissionService.getByRoles(userRoles.map((e) => e.id)) : [];
let rolePermissionStrings = rolePermissions.map((e) => e.permission);
let permissionObject = PermissionHelper.convertToObject([...userPermissionStrings, ...rolePermissionStrings]);
let jwtData: JWTToken = {
userId: id,
mail: mail,
username: username,
firstname: firstname,
lastname: lastname,
isOwner: isOwner,
permissions: permissionObject,
};
return await JWTHelper.create(jwtData)
.then((result) => {
return result;
})
.catch((err) => {
throw new InternalException("Failed accessToken creation", err);
});
}
}

View file

@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
export class ResetToken1732358596823 implements MigrationInterface {
name = "ResetToken1732358596823";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "reset",
columns: [
{ name: "mail", type: "varchar", length: "255", isPrimary: true, isNullable: false },
{ name: "token", type: "varchar", length: "255", isNullable: false },
{ name: "username", type: "varchar", length: "255", isNullable: false },
{ name: "secret", type: "varchar", length: "255", isNullable: false },
],
}),
true
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("reset");
}
}

View file

@ -8,6 +8,7 @@ import errorHandler from "../middleware/errorHandler";
import publicAvailable from "./public";
import setup from "./setup";
import reset from "./reset";
import auth from "./auth";
import admin from "./admin/index";
import user from "./user";
@ -25,6 +26,7 @@ export default (app: Express) => {
app.use("/public", publicAvailable);
app.use("/setup", allowSetup, setup);
app.use("/reset", reset);
app.use("/auth", auth);
app.use(authenticate);
app.use("/admin", admin);

19
src/routes/reset.ts Normal file
View file

@ -0,0 +1,19 @@
import express from "express";
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
import { finishReset, startReset, verifyReset } from "../controller/resetController";
var router = express.Router({ mergeParams: true });
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
await verifyReset(req, res);
});
router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"]), async (req, res) => {
await startReset(req, res);
});
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
await finishReset(req, res);
});
export default router;

View file

@ -0,0 +1,26 @@
import { dataSource } from "../data-source";
import { reset } from "../entity/reset";
import InternalException from "../exceptions/internalException";
export default abstract class ResetService {
/**
* @description get reset by id
* @param mail string
* @param token string
* @returns {Promise<reset>}
*/
static async getByMailAndToken(mail: string, token: string): Promise<reset> {
return await dataSource
.getRepository(reset)
.createQueryBuilder("reset")
.where("reset.mail = :mail", { mail: mail })
.andWhere("reset.token = :token", { token: token })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("reset not found by mail and token", err);
});
}
}