Skip to content

Feature/password by role #347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { ReferenceIdInterface } from '../../../reference/interfaces/reference-id.interface';
import { UserRolesInterface } from '../../user/interfaces/user-roles.interface';

export interface AuthenticatedUserInterface extends ReferenceIdInterface {}
export interface AuthenticatedUserInterface
extends ReferenceIdInterface,
Partial<UserRolesInterface> {}
4 changes: 4 additions & 0 deletions packages/nestjs-common/src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { AuthorizationPayloadInterface } from './authorization/interfaces/author

export { PasswordPlainCurrentInterface } from './password/interfaces/password-plain-current.interface';
export { PasswordPlainInterface } from './password/interfaces/password-plain.interface';
export { PasswordStrengthTransformOptionsInterface } from './password/interfaces/password-strength-transform-options.interface';

export { OrgCreatableInterface } from './org/interfaces/org-creatable.interface';
export { OrgOwnableInterface } from './org/interfaces/org-ownable.interface';
Expand All @@ -28,6 +29,8 @@ export { UserOwnableInterface } from './user/interfaces/user-ownable.interface';
export { UserUpdatableInterface } from './user/interfaces/user-updatable.interface';
export { UserInterface } from './user/interfaces/user.interface';

export { UserRolesInterface } from './user/interfaces/user-roles.interface';

export { FederatedCreatableInterface } from './federated/interfaces/federated-creatable.interface';
export { FederatedUpdatableInterface } from './federated/interfaces/federated-updatable.interface';
export { FederatedInterface } from './federated/interfaces/federated.interface';
Expand All @@ -37,6 +40,7 @@ export { RoleAssignmentCreatableInterface } from './role/interfaces/role-assignm
export { RoleAssignmentInterface } from './role/interfaces/role-assignment.interface';
export { RoleCreatableInterface } from './role/interfaces/role-creatable.interface';
export { RoleUpdatableInterface } from './role/interfaces/role-updatable.interface';
export { RoleOwnableInterface } from './role/interfaces/role-ownable.interface';
export { RoleInterface } from './role/interfaces/role.interface';

export { OtpClearInterface } from './otp/interfaces/otp-clear.interface';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RoleOwnableInterface } from '../../role/interfaces/role-ownable.interface';

export interface PasswordStrengthTransformOptionsInterface {
roles?: RoleOwnableInterface[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ReferenceId } from '../../../reference/interfaces/reference.types';
import { RoleInterface } from './role.interface';

export interface RoleOwnableInterface {
roleId: ReferenceId;
role?: Partial<RoleInterface>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { UserInterface } from './user.interface';
import { PasswordPlainInterface } from '../../password/interfaces/password-plain.interface';
import { UserRolesInterface } from './user-roles.interface';

export interface UserCreatableInterface
extends Pick<UserInterface, 'username' | 'email'>,
Partial<Pick<UserInterface, 'active'>>,
Partial<PasswordPlainInterface> {}
Partial<PasswordPlainInterface>,
Partial<UserRolesInterface> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RoleOwnableInterface } from '../../role/interfaces/role-ownable.interface';

export interface UserRolesInterface {
userRoles?: RoleOwnableInterface[];
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UserCreatableInterface } from './user-creatable.interface';
import { PasswordPlainCurrentInterface } from '../../password/interfaces/password-plain-current.interface';
import { UserRolesInterface } from './user-roles.interface';

export interface UserUpdatableInterface
extends Partial<
Pick<UserCreatableInterface, 'email' | 'password' | 'active'>
>,
Partial<PasswordPlainCurrentInterface> {}
Partial<PasswordPlainCurrentInterface>,
Partial<UserRolesInterface> {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PasswordStrengthEnum } from '../enum/password-strength.enum';

export interface PasswordCreateObjectOptionsInterface {
/**
* Optional salt. If not provided, one will be generated.
Expand All @@ -8,4 +10,10 @@ export interface PasswordCreateObjectOptionsInterface {
* Set to true if password is required.
*/
required?: boolean;

/**
* Optional password strength requirement. If provided, will validate
* that password meets minimum strength requirements.
*/
passwordStrength?: PasswordStrengthEnum | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PasswordStrengthEnum } from '../enum/password-strength.enum';

/**
* Password Strength Options Interface
*/
export interface PasswordStrengthOptionsInterface {
passwordStrength?: PasswordStrengthEnum | undefined;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { PasswordStrengthOptionsInterface } from './password-strength-options.interface';

/**
* Password Strength Service Interface
*/
Expand All @@ -6,6 +8,10 @@ export interface PasswordStrengthServiceInterface {
* Check if Password is strong
*
* @param password - The plain text password
* @param options - The options
*/
isStrong(password: string): boolean;
isStrong(
password: string,
options?: PasswordStrengthOptionsInterface,
): boolean;
}
36 changes: 25 additions & 11 deletions packages/nestjs-password/src/services/password-creation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { PasswordHistoryPasswordInterface } from '../interfaces/password-history
import { PasswordNotStrongException } from '../exceptions/password-not-strong.exception';
import { PasswordCurrentRequiredException } from '../exceptions/password-current-required.exception';
import { PasswordUsedRecentlyException } from '../exceptions/password-used-recently.exception';
import { PasswordException } from '../exceptions/password.exception';

/**
* Service with functions related to password creation
Expand Down Expand Up @@ -74,19 +75,32 @@ export class PasswordCreationService
): Promise<
Omit<T, 'password'> | (Omit<T, 'password'> & PasswordStorageInterface)
> {
// extract properties
const { password } = object;

// is the password in the object?
if (typeof password === 'string') {
// check strength
if (!this.passwordStrengthService.isStrong(password)) {
throw new PasswordNotStrongException();
try {
// extract properties
const { password } = object;

// is the password in the object?
if (typeof password === 'string') {
// check strength
if (
!this.passwordStrengthService.isStrong(password, {
passwordStrength: options?.passwordStrength,
})
) {
throw new PasswordNotStrongException();
}
}
}

// finally hash it
return this.passwordStorageService.hashObject(object, options);
return this.passwordStorageService.hashObject(object, options);
} catch (err) {
if (err instanceof PasswordNotStrongException) {
throw err;
}
throw new PasswordException({
message: 'Failed to create password',
originalError: err,
});
}
}

public async validateCurrent(
Expand Down
26 changes: 20 additions & 6 deletions packages/nestjs-password/src/services/password-strength.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PASSWORD_MODULE_SETTINGS_TOKEN } from '../password.constants';
import { PasswordStrengthEnum } from '../enum/password-strength.enum';
import { PasswordStrengthServiceInterface } from '../interfaces/password-strength-service.interface';
import { PasswordSettingsInterface } from '../interfaces/password-settings.interface';
import { PasswordStrengthOptionsInterface } from '../interfaces/password-strength-options.interface';

/**
* Service to validate password strength
Expand All @@ -23,15 +24,28 @@ export class PasswordStrengthService
) {}

/**
* Method to check if password is strong
* Check if a password meets the minimum strength requirements.
* Uses zxcvbn to score password strength from 0-4:
*
* @param password - the plain text password
* @returns password strength
* The minimum required strength can be specified via:
* 1. The options.passwordStrength parameter - If defined it will be used as the minimum required strength
* 2. The module settings minPasswordStrength - Global minimum strength setting
* 3. Defaults to PasswordStrengthEnum.None (0) - If no other strength requirements specified
*
* @param password - The password to check
* @param options - Optional strength validation options
* @returns True if password meets minimum strength, false otherwise
*/
isStrong(password: string): boolean {
// Get min password Strength
isStrong(
password: string,
options?: PasswordStrengthOptionsInterface,
): boolean {
const { passwordStrength } = options || {};

const minStrength =
this.settings?.minPasswordStrength || PasswordStrengthEnum.None;
passwordStrength ??
this.settings?.minPasswordStrength ??
PasswordStrengthEnum.None;

// check strength of the password
const result = zxcvbn(password);
Expand Down
4 changes: 4 additions & 0 deletions packages/nestjs-user/src/config/user-default.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
USER_MODULE_DEFAULT_SETTINGS_TOKEN,
USER_MODULE_USER_PASSWORD_HISTORY_LIMIT_DAYS_DEFAULT,
} from '../user.constants';
import { defaultPasswordStrengthTransform } from '../user.utils';

/**
* Default configuration for User module.
Expand All @@ -24,6 +25,9 @@ export const userDefaultConfig = registerAs(
enabled,
limitDays: isNaN(limitDays) || limitDays < 1 ? undefined : limitDays,
},
passwordStrength: {
passwordStrengthTransform: defaultPasswordStrengthTransform,
},
};
},
);
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/dto/user-create.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntersectionType, PartialType, PickType } from '@nestjs/swagger';
import { UserCreatableInterface } from '@concepta/nestjs-common';
import { UserDto } from './user.dto';
import { UserPasswordDto } from './user-password.dto';
import { UserRolesDto } from './user-roles.dto';

/**
* User Create DTO
Expand All @@ -13,5 +14,6 @@ export class UserCreateDto
PickType(UserDto, ['username', 'email'] as const),
PartialType(PickType(UserDto, ['active'] as const)),
PartialType(UserPasswordDto),
PartialType(UserRolesDto),
)
implements UserCreatableInterface {}
17 changes: 17 additions & 0 deletions packages/nestjs-user/src/dto/user-roles.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
RoleOwnableInterface,
UserRolesInterface,
} from '@concepta/nestjs-common';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Exclude, Expose } from 'class-transformer';

@Exclude()
export class UserRolesDto implements UserRolesInterface {
@Expose()
@ApiPropertyOptional({
type: 'array',
isArray: true,
description: 'User roles',
})
userRoles?: RoleOwnableInterface[];
}
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/dto/user-update.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { UserUpdatableInterface } from '@concepta/nestjs-common';
import { UserDto } from './user.dto';
import { UserPasswordDto } from './user-password.dto';
import { UserPasswordUpdateDto } from './user-password-update.dto';
import { UserRolesDto } from './user-roles.dto';

/**
* User Update DTO
Expand All @@ -14,5 +15,6 @@ export class UserUpdateDto
PartialType(PickType(UserDto, ['email', 'active'] as const)),
PartialType(UserPasswordDto),
PartialType(UserPasswordUpdateDto),
PartialType(UserRolesDto),
)
implements UserUpdatableInterface {}
3 changes: 3 additions & 0 deletions packages/nestjs-user/src/entities/user-postgres.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column } from 'typeorm';
import { CommonPostgresEntity } from '@concepta/typeorm-common';
import { UserEntityInterface } from '../interfaces/user-entity.interface';
import { UserPasswordHistoryEntityInterface } from '../interfaces/user-password-history-entity.interface';
import { RoleOwnableInterface } from '@concepta/nestjs-common';

/**
* User Entity
Expand Down Expand Up @@ -40,5 +41,7 @@ export abstract class UserPostgresEntity
@Column({ type: 'text', nullable: true, default: null })
passwordSalt: string | null = null;

userRoles?: RoleOwnableInterface[];

userPasswordHistory?: UserPasswordHistoryEntityInterface;
}
3 changes: 3 additions & 0 deletions packages/nestjs-user/src/entities/user-sqlite.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Column } from 'typeorm';
import { CommonSqliteEntity } from '@concepta/typeorm-common';
import { UserEntityInterface } from '../interfaces/user-entity.interface';
import { UserPasswordHistoryEntityInterface } from '../interfaces/user-password-history-entity.interface';
import { RoleOwnableInterface } from '@concepta/nestjs-common';

export abstract class UserSqliteEntity
extends CommonSqliteEntity
Expand Down Expand Up @@ -37,5 +38,7 @@ export abstract class UserSqliteEntity
@Column({ type: 'text', nullable: true, default: null })
passwordSalt: string | null = null;

userRoles?: RoleOwnableInterface[];

userPasswordHistory?: UserPasswordHistoryEntityInterface;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { UserException } from './user-exception';

export class UserRolePasswordException extends UserException {
constructor(options?: RuntimeExceptionOptions) {
super({
message: 'Unable to get password strength for role',
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'USER_ROLES_ERROR';
}
}
16 changes: 16 additions & 0 deletions packages/nestjs-user/src/exceptions/user-roles-exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { RuntimeExceptionOptions } from '@concepta/nestjs-exception';
import { HttpStatus } from '@nestjs/common';
import { UserException } from './user-exception';

export class UserRolesException extends UserException {
constructor(userId: string, options?: RuntimeExceptionOptions) {
super({
message: 'Unable to get user roles for user ${userId}',
messageParams: [userId],
httpStatus: HttpStatus.BAD_REQUEST,
...options,
});

this.errorCode = 'USER_ROLES_ERROR';
}
}
1 change: 1 addition & 0 deletions packages/nestjs-user/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export { UserResource } from './user.types';
export { UserException } from './exceptions/user-exception';
export { UserBadRequestException } from './exceptions/user-bad-request-exception';
export { UserNotFoundException } from './exceptions/user-not-found-exception';
export { UserRolesException } from './exceptions/user-roles-exception';
5 changes: 3 additions & 2 deletions packages/nestjs-user/src/interfaces/user-entity.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { UserInterface } from '@concepta/nestjs-common';
import { UserInterface, UserRolesInterface } from '@concepta/nestjs-common';
import { PasswordStorageInterface } from '@concepta/nestjs-password';

export interface UserEntityInterface
extends UserInterface,
PasswordStorageInterface {}
PasswordStorageInterface,
UserRolesInterface {}
2 changes: 2 additions & 0 deletions packages/nestjs-user/src/interfaces/user-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { UserLookupServiceInterface } from './user-lookup-service.interface';
import { UserMutateServiceInterface } from './user-mutate-service.interface';
import { UserPasswordServiceInterface } from './user-password-service.interface';
import { UserPasswordHistoryServiceInterface } from './user-password-history-service.interface';
import { UserRoleServiceInterface } from './user-role-service.interface';

export interface UserOptionsInterface {
settings?: UserSettingsInterface;
userLookupService?: UserLookupServiceInterface;
userMutateService?: UserMutateServiceInterface;
userPasswordService?: UserPasswordServiceInterface;
userRoleService?: UserRoleServiceInterface;
userPasswordHistoryService?: UserPasswordHistoryServiceInterface;
userAccessQueryService?: CanAccess;
}
Loading