From 6042089074810df8b5af8fc5ff6447ea8c1dc7d0 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:24:25 +0100 Subject: [PATCH 01/16] Added pw reset jwt generation ref #40 --- src/jwtcreator.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/jwtcreator.ts b/src/jwtcreator.ts index e837f13..eb33101 100644 --- a/src/jwtcreator.ts +++ b/src/jwtcreator.ts @@ -33,6 +33,20 @@ export class JwtCreator { exp: expiry_timestamp }, config.jwt_secret) } + + /** + * Creates a new password reset token for a given user. + * The token is valid for 15 minutes or 1 use - whatever comes first. + * @param user User entity that the password reset token shall be created for + */ + public static createReset(user: User) { + let expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; + return jsonwebtoken.sign({ + id: user.id, + refreshTokenCount: user.refreshTokenCount, + exp: expiry_timestamp + }, config.jwt_secret) + } } /** -- 2.47.2 From aa146cd6c1d2bfd94217778863f55b34c9c3b103 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:38:48 +0100 Subject: [PATCH 02/16] Added a basic pw reset action ref #40 --- src/models/actions/ResetPassword.ts | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/models/actions/ResetPassword.ts diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts new file mode 100644 index 0000000..186f4cb --- /dev/null +++ b/src/models/actions/ResetPassword.ts @@ -0,0 +1,46 @@ +import { IsEmail, IsOptional, IsString } from 'class-validator'; +import { getConnectionManager } from 'typeorm'; +import { UserNotFoundError } from '../../errors/AuthError'; +import { UsernameOrEmailNeededError } from '../../errors/UserErrors'; +import { JwtCreator } from '../../jwtcreator'; +import { User } from '../entities/User'; + +/** + * TODO: + */ +export class ResetPassword { + /** + * The username of the user that want's to login. + * Either username or email have to be provided. + */ + @IsOptional() + @IsString() + username?: string; + + /** + * The email address of the user that want's to login. + * Either username or email have to be provided. + */ + @IsOptional() + @IsEmail() + @IsString() + email?: string; + + + /** + * Reset a password based on this. + */ + public async toResetToken(): Promise { + if (this.email === undefined && this.username === undefined) { + throw new UsernameOrEmailNeededError(); + } + const found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] }); + if (!found_user) { + throw new UserNotFoundError(); + } + + //Create the reset + let access_token = JwtCreator.createReset(found_user); + return access_token; + } +} \ No newline at end of file -- 2.47.2 From 61aff5e629c0e1c9349e4709bcbeb0b3e56ec191 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:39:17 +0100 Subject: [PATCH 03/16] Added a password reset token request route ref #40 --- src/controllers/AuthController.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index d6d8d81..022bffd 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -3,6 +3,7 @@ import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError'; import { UserNotFoundError } from '../errors/UserErrors'; import { CreateAuth } from '../models/actions/CreateAuth'; +import { CreateResetToken } from '../models/actions/CreateResetToken'; import { HandleLogout } from '../models/actions/HandleLogout'; import { RefreshAuth } from '../models/actions/RefreshAuth'; import { Auth } from '../models/responses/ResponseAuth'; @@ -78,4 +79,13 @@ export class AuthController { } return response.send(auth) } + + @Post("/reset") + @ResponseSchema(Auth) + @ResponseSchema(UserNotFoundError) + @ResponseSchema(UsernameOrEmailNeededError) + @OpenAPI({ description: "Request a password reset token" }) + async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) { + return await passwordReset.toResetToken(); + } } -- 2.47.2 From aef8485f597aca09e680a3967bd15b361c1531c4 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:39:42 +0100 Subject: [PATCH 04/16] Renamed the password reset token creation class to better fit the scheme ref #40 --- .../{ResetPassword.ts => CreateResetToken.ts} | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) rename src/models/actions/{ResetPassword.ts => CreateResetToken.ts} (61%) diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/CreateResetToken.ts similarity index 61% rename from src/models/actions/ResetPassword.ts rename to src/models/actions/CreateResetToken.ts index 186f4cb..0e32aad 100644 --- a/src/models/actions/ResetPassword.ts +++ b/src/models/actions/CreateResetToken.ts @@ -8,18 +8,16 @@ import { User } from '../entities/User'; /** * TODO: */ -export class ResetPassword { +export class CreateResetToken { /** - * The username of the user that want's to login. - * Either username or email have to be provided. + * The username of the user that wants to reset their password. */ @IsOptional() @IsString() username?: string; /** - * The email address of the user that want's to login. - * Either username or email have to be provided. + * The email address of the user that wants to reset their password. */ @IsOptional() @IsEmail() @@ -28,17 +26,20 @@ export class ResetPassword { /** - * Reset a password based on this. + * Create a password reset token based on this. */ public async toResetToken(): Promise { if (this.email === undefined && this.username === undefined) { throw new UsernameOrEmailNeededError(); } - const found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] }); + let found_user = await getConnectionManager().get().getRepository(User).findOne({ relations: ['groups', 'permissions', 'actions'], where: [{ username: this.username }, { email: this.email }] }); if (!found_user) { throw new UserNotFoundError(); } + found_user.refreshTokenCount = found_user.refreshTokenCount + 1; + await getConnectionManager().get().getRepository(User).save(found_user); + //Create the reset let access_token = JwtCreator.createReset(found_user); return access_token; -- 2.47.2 From 5aa83fe2f0b3c2afbcde2114a60163929c651e12 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:44:43 +0100 Subject: [PATCH 05/16] Renamed the return variable to fit the class ref #40 --- src/models/actions/CreateResetToken.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/actions/CreateResetToken.ts b/src/models/actions/CreateResetToken.ts index 0e32aad..4a5fe52 100644 --- a/src/models/actions/CreateResetToken.ts +++ b/src/models/actions/CreateResetToken.ts @@ -41,7 +41,7 @@ export class CreateResetToken { await getConnectionManager().get().getRepository(User).save(found_user); //Create the reset - let access_token = JwtCreator.createReset(found_user); - return access_token; + let reset_token = JwtCreator.createReset(found_user); + return reset_token; } } \ No newline at end of file -- 2.47.2 From caeb17311b3acae85850f4dd0fe835421f6d2b63 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:57:08 +0100 Subject: [PATCH 06/16] Implemented basic password reset ref #40 --- src/models/actions/ResetPassword.ts | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/models/actions/ResetPassword.ts diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts new file mode 100644 index 0000000..d491156 --- /dev/null +++ b/src/models/actions/ResetPassword.ts @@ -0,0 +1,60 @@ +import * as argon2 from "argon2"; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import * as jsonwebtoken from 'jsonwebtoken'; +import { getConnectionManager } from 'typeorm'; +import { config } from '../../config'; +import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError'; +import { User } from '../entities/User'; + +/** + * TODO: + */ +export class ResetPassword { + /** + * The reset token on which the password reset will be based. + */ + @IsOptional() + @IsString() + resetToken?: string; + + /** + * The user's new password + */ + @IsNotEmpty() + @IsString() + password: string; + + + /** + * Create a password reset token based on this. + */ + public async resetPassword(): Promise { + if (!this.resetToken || this.resetToken === undefined) { + throw new JwtNotProvidedError() + } + if (!this.password || this.password === undefined) { + throw new PasswordNeededError() + } + + let decoded; + try { + decoded = jsonwebtoken.verify(this.resetToken, config.jwt_secret) + } catch (error) { + throw new IllegalJWTError() + } + + const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }); + if (!found_user) { + throw new UserNotFoundError() + } + if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { + throw new RefreshTokenCountInvalidError() + } + + found_user.refreshTokenCount = found_user.refreshTokenCount + 1; + found_user.password = await argon2.hash(this.password + found_user.uuid); + await getConnectionManager().get().getRepository(User).save(found_user); + + return "password reset successfull"; + } +} \ No newline at end of file -- 2.47.2 From 5aad581c2d01fc674c0f94a7c6a778b798abaa07 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 10:57:25 +0100 Subject: [PATCH 07/16] Implemented toe password reset route ref #40 --- src/controllers/AuthController.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index 022bffd..a141a6c 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,4 +1,4 @@ -import { Body, CookieParam, JsonController, Post, Res } from 'routing-controllers'; +import { Body, CookieParam, JsonController, Param, Post, Res } from 'routing-controllers'; import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi'; import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError'; import { UserNotFoundError } from '../errors/UserErrors'; @@ -6,6 +6,7 @@ import { CreateAuth } from '../models/actions/CreateAuth'; import { CreateResetToken } from '../models/actions/CreateResetToken'; import { HandleLogout } from '../models/actions/HandleLogout'; import { RefreshAuth } from '../models/actions/RefreshAuth'; +import { ResetPassword } from '../models/actions/ResetPassword'; import { Auth } from '../models/responses/ResponseAuth'; import { Logout } from '../models/responses/ResponseLogout'; @@ -86,6 +87,16 @@ export class AuthController { @ResponseSchema(UsernameOrEmailNeededError) @OpenAPI({ description: "Request a password reset token" }) async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) { - return await passwordReset.toResetToken(); + return { "resetToken": await passwordReset.toResetToken() }; + } + + @Post("/reset/:token") + @ResponseSchema(Auth) + @ResponseSchema(UserNotFoundError) + @ResponseSchema(UsernameOrEmailNeededError) + @OpenAPI({ description: "Reset a user's password" }) + async resetPassword(@Param("token") token: string, @Body({ validate: true }) passwordReset: ResetPassword) { + passwordReset.resetToken = token; + return await passwordReset.resetPassword(); } } -- 2.47.2 From 48685451bead5972a95dec9abd03f9b5285454ed Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:07:01 +0100 Subject: [PATCH 08/16] Set reset token expiry to 15 mins rer #40 --- src/jwtcreator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jwtcreator.ts b/src/jwtcreator.ts index eb33101..4290aec 100644 --- a/src/jwtcreator.ts +++ b/src/jwtcreator.ts @@ -40,7 +40,7 @@ export class JwtCreator { * @param user User entity that the password reset token shall be created for */ public static createReset(user: User) { - let expiry_timestamp = Math.floor(Date.now() / 1000) + 10 * 36000; + let expiry_timestamp = Math.floor(Date.now() / 1000) + 15 * 60; return jsonwebtoken.sign({ id: user.id, refreshTokenCount: user.refreshTokenCount, -- 2.47.2 From 17ee682029cf261a557dc2e20d6375c41b12f721 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:12:24 +0100 Subject: [PATCH 09/16] Implemented a password reset timeout ref #40 --- src/errors/AuthError.ts | 11 +++++++++++ src/models/actions/CreateResetToken.ts | 7 +++++-- src/models/entities/User.ts | 9 +++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/errors/AuthError.ts b/src/errors/AuthError.ts index c2eff13..fd78712 100644 --- a/src/errors/AuthError.ts +++ b/src/errors/AuthError.ts @@ -115,4 +115,15 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError { @IsString() message = "Refresh token count is invalid." +} + +/** + * Error to throw when someone tryes to refresh a user's password more than once in 15 minutes. + */ +export class ResetAlreadyRequestedError extends NotAcceptableError { + @IsString() + name = "ResetAlreadyRequestedError" + + @IsString() + message = "You already requested a password reset in the last 15 minutes. \n Please wait until the old reset code expires before requesting a new one." } \ No newline at end of file diff --git a/src/models/actions/CreateResetToken.ts b/src/models/actions/CreateResetToken.ts index 4a5fe52..b8974d9 100644 --- a/src/models/actions/CreateResetToken.ts +++ b/src/models/actions/CreateResetToken.ts @@ -1,6 +1,6 @@ import { IsEmail, IsOptional, IsString } from 'class-validator'; import { getConnectionManager } from 'typeorm'; -import { UserNotFoundError } from '../../errors/AuthError'; +import { ResetAlreadyRequestedError, UserNotFoundError } from '../../errors/AuthError'; import { UsernameOrEmailNeededError } from '../../errors/UserErrors'; import { JwtCreator } from '../../jwtcreator'; import { User } from '../entities/User'; @@ -32,12 +32,15 @@ export class CreateResetToken { if (this.email === undefined && this.username === undefined) { throw new UsernameOrEmailNeededError(); } - let found_user = await getConnectionManager().get().getRepository(User).findOne({ relations: ['groups', 'permissions', 'actions'], where: [{ username: this.username }, { email: this.email }] }); + let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] }); if (!found_user) { throw new UserNotFoundError(); } + if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); } + found_user.refreshTokenCount = found_user.refreshTokenCount + 1; + found_user.resetRequestedTimestamp = Math.floor(Date.now() / 1000); await getConnectionManager().get().getRepository(User).save(found_user); //Create the reset diff --git a/src/models/entities/User.ts b/src/models/entities/User.ts index 4cafa8d..57bdf5c 100644 --- a/src/models/entities/User.ts +++ b/src/models/entities/User.ts @@ -111,6 +111,15 @@ export class User extends Principal { @IsOptional() profilePic?: string; + /** + * The last time the user requested a password reset. + * Used to prevent spamming of the password reset route. + */ + @Column({ nullable: true, unique: true }) + @IsString() + @IsOptional() + resetRequestedTimestamp?: number; + /** * The actions performed by this user. * For documentation purposes only, will be implemented later. -- 2.47.2 From 4b9bfe3b79c2afd4013df5ed30d7e3fa5b635e2e Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:18:31 +0100 Subject: [PATCH 10/16] Now disableing users while they're in the process of resetting their password ref #40 --- src/errors/AuthError.ts | 13 ++++++++++++- src/models/actions/CreateResetToken.ts | 9 ++++----- src/models/actions/ResetPassword.ts | 12 +++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/errors/AuthError.ts b/src/errors/AuthError.ts index fd78712..848ce78 100644 --- a/src/errors/AuthError.ts +++ b/src/errors/AuthError.ts @@ -118,7 +118,7 @@ export class RefreshTokenCountInvalidError extends NotAcceptableError { } /** - * Error to throw when someone tryes to refresh a user's password more than once in 15 minutes. + * Error to throw when someone tryes to reset a user's password more than once in 15 minutes. */ export class ResetAlreadyRequestedError extends NotAcceptableError { @IsString() @@ -126,4 +126,15 @@ export class ResetAlreadyRequestedError extends NotAcceptableError { @IsString() message = "You already requested a password reset in the last 15 minutes. \n Please wait until the old reset code expires before requesting a new one." +} + +/** + * Error to throw when someone tries a disabled user's password or login as a disabled user. + */ +export class UserDisabledError extends NotAcceptableError { + @IsString() + name = "UserDisabledError" + + @IsString() + message = "This user is currently disabled. \n Please contact your administrator if this is a mistake." } \ No newline at end of file diff --git a/src/models/actions/CreateResetToken.ts b/src/models/actions/CreateResetToken.ts index b8974d9..2c7f6db 100644 --- a/src/models/actions/CreateResetToken.ts +++ b/src/models/actions/CreateResetToken.ts @@ -1,6 +1,6 @@ import { IsEmail, IsOptional, IsString } from 'class-validator'; import { getConnectionManager } from 'typeorm'; -import { ResetAlreadyRequestedError, UserNotFoundError } from '../../errors/AuthError'; +import { ResetAlreadyRequestedError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError'; import { UsernameOrEmailNeededError } from '../../errors/UserErrors'; import { JwtCreator } from '../../jwtcreator'; import { User } from '../entities/User'; @@ -33,14 +33,13 @@ export class CreateResetToken { throw new UsernameOrEmailNeededError(); } let found_user = await getConnectionManager().get().getRepository(User).findOne({ where: [{ username: this.username }, { email: this.email }] }); - if (!found_user) { - throw new UserNotFoundError(); - } - + if (!found_user) { throw new UserNotFoundError(); } + if (found_user.enabled == false) { throw new UserDisabledError(); } if (found_user.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 15 * 60)) { throw new ResetAlreadyRequestedError(); } found_user.refreshTokenCount = found_user.refreshTokenCount + 1; found_user.resetRequestedTimestamp = Math.floor(Date.now() / 1000); + found_user.enabled = false; await getConnectionManager().get().getRepository(User).save(found_user); //Create the reset diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts index d491156..167bb7f 100644 --- a/src/models/actions/ResetPassword.ts +++ b/src/models/actions/ResetPassword.ts @@ -3,7 +3,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import * as jsonwebtoken from 'jsonwebtoken'; import { getConnectionManager } from 'typeorm'; import { config } from '../../config'; -import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError'; +import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError'; import { User } from '../entities/User'; /** @@ -44,15 +44,13 @@ export class ResetPassword { } const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }); - if (!found_user) { - throw new UserNotFoundError() - } - if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { - throw new RefreshTokenCountInvalidError() - } + if (!found_user) { throw new UserNotFoundError(); } + if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { throw new RefreshTokenCountInvalidError(); } + if (found_user.enabled == false) { throw new UserDisabledError(); } found_user.refreshTokenCount = found_user.refreshTokenCount + 1; found_user.password = await argon2.hash(this.password + found_user.uuid); + found_user.enabled = true; await getConnectionManager().get().getRepository(User).save(found_user); return "password reset successfull"; -- 2.47.2 From 2f7b0d5606de7daa168d6db4f081df4169641e87 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:20:11 +0100 Subject: [PATCH 11/16] Removed bs enabled check ref #40 --- src/models/actions/ResetPassword.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts index 167bb7f..1ceca59 100644 --- a/src/models/actions/ResetPassword.ts +++ b/src/models/actions/ResetPassword.ts @@ -3,7 +3,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; import * as jsonwebtoken from 'jsonwebtoken'; import { getConnectionManager } from 'typeorm'; import { config } from '../../config'; -import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError'; +import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError'; import { User } from '../entities/User'; /** @@ -46,7 +46,6 @@ export class ResetPassword { const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["id"] }); if (!found_user) { throw new UserNotFoundError(); } if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { throw new RefreshTokenCountInvalidError(); } - if (found_user.enabled == false) { throw new UserDisabledError(); } found_user.refreshTokenCount = found_user.refreshTokenCount + 1; found_user.password = await argon2.hash(this.password + found_user.uuid); -- 2.47.2 From 8d860cb2e109bcee81d0de60cdbbcbe9076a8fde Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:26:45 +0100 Subject: [PATCH 12/16] Fixed weired query behaviour ref #40 --- src/controllers/UserController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index e4a8e89..560f0c8 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -62,7 +62,7 @@ export class UserController { } user = await this.userRepository.save(user) - return new ResponseUser(await this.userRepository.findOne(user, { relations: ['permissions', 'groups'] })); + return new ResponseUser(await this.userRepository.findOne({ id: user.id }, { relations: ['permissions', 'groups'] })); } @Put('/:id') -- 2.47.2 From a16c4c564a5c81fbe46326591ca574c09069fcee Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:27:21 +0100 Subject: [PATCH 13/16] Users now can be disabled from the start ref #40 --- src/models/actions/CreateUser.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/models/actions/CreateUser.ts b/src/models/actions/CreateUser.ts index 3839f11..0203d99 100644 --- a/src/models/actions/CreateUser.ts +++ b/src/models/actions/CreateUser.ts @@ -1,5 +1,5 @@ import * as argon2 from "argon2"; -import { IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsOptional, IsPhoneNumber, IsString } from 'class-validator'; import { getConnectionManager } from 'typeorm'; import * as uuid from 'uuid'; import { config } from '../../config'; @@ -63,6 +63,14 @@ export class CreateUser { @IsString() password: string; + /** + * Will the new user be enabled from the start? + * Default: true + */ + @IsBoolean() + @IsOptional() + enabled?: boolean = true; + /** * The new user's groups' id(s). * You can provide either one groupId or an array of groupIDs. @@ -91,6 +99,7 @@ export class CreateUser { newUser.phone = this.phone newUser.password = await argon2.hash(this.password + newUser.uuid); newUser.groups = await this.getGroups(); + newUser.enabled = this.enabled; //TODO: ProfilePics return newUser; -- 2.47.2 From bf4250babd3e5c684cfdea9d49a5e268db8867c8 Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:29:52 +0100 Subject: [PATCH 14/16] All things auth now check if the user is disabled ref #40 --- src/authchecker.ts | 4 +++- src/models/actions/CreateAuth.ts | 3 ++- src/models/actions/RefreshAuth.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/authchecker.ts b/src/authchecker.ts index 36cb40b..54ef4d7 100644 --- a/src/authchecker.ts +++ b/src/authchecker.ts @@ -3,7 +3,7 @@ import * as jwt from "jsonwebtoken"; import { Action } from "routing-controllers"; import { getConnectionManager } from 'typeorm'; import { config } from './config'; -import { IllegalJWTError, NoPermissionError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError'; +import { IllegalJWTError, NoPermissionError, UserDisabledError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError'; import { JwtCreator, JwtUser } from './jwtcreator'; import { User } from './models/entities/User'; @@ -31,6 +31,7 @@ const authchecker = async (action: Action, permissions: string[] | string) => { const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions'] }) if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() } + if (user.enabled == false) { throw new UserDisabledError(); } if (!jwtPayload["permissions"]) { throw new NoPermissionError(); } action.response.local = {} @@ -63,6 +64,7 @@ const refresh = async (action: Action) => { const user = await getConnectionManager().get().getRepository(User).findOne({ id: jwtPayload["id"], refreshTokenCount: jwtPayload["refreshTokenCount"] }, { relations: ['permissions', 'groups', 'groups.permissions'] }) if (!user) { throw new UserNonexistantOrRefreshtokenInvalidError() } + if (user.enabled == false) { throw new UserDisabledError(); } let newAccess = JwtCreator.createAccess(user); action.response.header("authorization", "Bearer " + newAccess); diff --git a/src/models/actions/CreateAuth.ts b/src/models/actions/CreateAuth.ts index dd9c3f3..6b22d7d 100644 --- a/src/models/actions/CreateAuth.ts +++ b/src/models/actions/CreateAuth.ts @@ -1,7 +1,7 @@ import * as argon2 from "argon2"; import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { getConnectionManager } from 'typeorm'; -import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError'; +import { InvalidCredentialsError, PasswordNeededError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError'; import { UsernameOrEmailNeededError } from '../../errors/UserErrors'; import { JwtCreator } from '../../jwtcreator'; import { User } from '../entities/User'; @@ -55,6 +55,7 @@ export class CreateAuth { if (!found_user) { throw new UserNotFoundError(); } + if (found_user.enabled == false) { throw new UserDisabledError(); } if (!(await argon2.verify(found_user.password, this.password + found_user.uuid))) { throw new InvalidCredentialsError(); } diff --git a/src/models/actions/RefreshAuth.ts b/src/models/actions/RefreshAuth.ts index 12470c7..bcc4fbb 100644 --- a/src/models/actions/RefreshAuth.ts +++ b/src/models/actions/RefreshAuth.ts @@ -2,7 +2,7 @@ import { IsOptional, IsString } from 'class-validator'; import * as jsonwebtoken from 'jsonwebtoken'; import { getConnectionManager } from 'typeorm'; import { config } from '../../config'; -import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError'; +import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserDisabledError, UserNotFoundError } from '../../errors/AuthError'; import { JwtCreator } from "../../jwtcreator"; import { User } from '../entities/User'; import { Auth } from '../responses/ResponseAuth'; @@ -39,6 +39,7 @@ export class RefreshAuth { if (!found_user) { throw new UserNotFoundError() } + if (found_user.enabled == false) { throw new UserDisabledError(); } if (found_user.refreshTokenCount !== decoded["refreshTokenCount"]) { throw new RefreshTokenCountInvalidError() } -- 2.47.2 From 9458b774ea1abd7d2c10676264e715ee5e44b49f Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:35:33 +0100 Subject: [PATCH 15/16] Removed the user disableing ref #40 --- src/models/actions/CreateResetToken.ts | 1 - src/models/actions/ResetPassword.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/models/actions/CreateResetToken.ts b/src/models/actions/CreateResetToken.ts index 2c7f6db..8e63b24 100644 --- a/src/models/actions/CreateResetToken.ts +++ b/src/models/actions/CreateResetToken.ts @@ -39,7 +39,6 @@ export class CreateResetToken { found_user.refreshTokenCount = found_user.refreshTokenCount + 1; found_user.resetRequestedTimestamp = Math.floor(Date.now() / 1000); - found_user.enabled = false; await getConnectionManager().get().getRepository(User).save(found_user); //Create the reset diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts index 1ceca59..e8229b3 100644 --- a/src/models/actions/ResetPassword.ts +++ b/src/models/actions/ResetPassword.ts @@ -49,7 +49,6 @@ export class ResetPassword { found_user.refreshTokenCount = found_user.refreshTokenCount + 1; found_user.password = await argon2.hash(this.password + found_user.uuid); - found_user.enabled = true; await getConnectionManager().get().getRepository(User).save(found_user); return "password reset successfull"; -- 2.47.2 From 146787fd660afe1bd8d775b9355e3ad97f6795dc Mon Sep 17 00:00:00 2001 From: Nicolai Ort Date: Tue, 22 Dec 2020 11:48:06 +0100 Subject: [PATCH 16/16] Added comments ref #40 --- src/controllers/AuthController.ts | 1 + src/models/actions/CreateResetToken.ts | 6 ++++-- src/models/actions/ResetPassword.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index a141a6c..98a9f85 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -87,6 +87,7 @@ export class AuthController { @ResponseSchema(UsernameOrEmailNeededError) @OpenAPI({ description: "Request a password reset token" }) async getResetToken(@Body({ validate: true }) passwordReset: CreateResetToken) { + //This really shouldn't just get returned, but sent via mail or sth like that. But for dev only this is fine. return { "resetToken": await passwordReset.toResetToken() }; } diff --git a/src/models/actions/CreateResetToken.ts b/src/models/actions/CreateResetToken.ts index 8e63b24..dcf22f1 100644 --- a/src/models/actions/CreateResetToken.ts +++ b/src/models/actions/CreateResetToken.ts @@ -6,7 +6,8 @@ import { JwtCreator } from '../../jwtcreator'; import { User } from '../entities/User'; /** - * TODO: + * This calss is used to create password reset tokens for users. + * These password reset token can be used to set a new password for the user for the next 15mins. */ export class CreateResetToken { /** @@ -41,8 +42,9 @@ export class CreateResetToken { found_user.resetRequestedTimestamp = Math.floor(Date.now() / 1000); await getConnectionManager().get().getRepository(User).save(found_user); - //Create the reset + //Create the reset token let reset_token = JwtCreator.createReset(found_user); + return reset_token; } } \ No newline at end of file diff --git a/src/models/actions/ResetPassword.ts b/src/models/actions/ResetPassword.ts index e8229b3..2dafc11 100644 --- a/src/models/actions/ResetPassword.ts +++ b/src/models/actions/ResetPassword.ts @@ -7,7 +7,8 @@ import { IllegalJWTError, JwtNotProvidedError, PasswordNeededError, RefreshToken import { User } from '../entities/User'; /** - * TODO: + * This class can be used to reset a user's password. + * To set a new password the user needs to provide a valid password reset token. */ export class ResetPassword { /** -- 2.47.2