Skip to content

Commit

Permalink
SPSH-1278 New password generation (#731)
Browse files Browse the repository at this point in the history
* New password generation
  • Loading branch information
marode-cap authored Nov 4, 2024
1 parent ffbf6e4 commit c5d17ad
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 25 deletions.
3 changes: 2 additions & 1 deletion src/modules/email/persistence/email.repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { mapAggregateToData } from './email.repo.js';
import { PersonAlreadyHasEnabledEmailAddressError } from '../error/person-already-has-enabled-email-address.error.js';
import { UserLockRepository } from '../../keycloak-administration/repository/user-lock.repository.js';
import { PersonEmailResponse } from '../../person/api/person-email-response.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('EmailRepo', () => {
let module: TestingModule;
Expand Down Expand Up @@ -91,7 +92,7 @@ describe('EmailRepo', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personResult instanceof DomainError) {
throw personResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { type FindUserFilter, KeycloakUserService } from './keycloak-user.servic
import { User } from './user.js';
import { UserLock } from './user-lock.js';
import { UserLockRepository } from '../repository/user-lock.repository.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('KeycloakUserService', () => {
let module: TestingModule;
Expand Down Expand Up @@ -728,10 +729,7 @@ describe('KeycloakUserService', () => {
describe('if password is temporary', () => {
it('should return result with ok:true and new temporary password', async () => {
const userId: string = faker.string.numeric();
const generatedPassword: string = faker.string.alphanumeric({
length: { min: 10, max: 10 },
casing: 'mixed',
});
const generatedPassword: string = generatePassword();
kcUsersMock.resetPassword.mockResolvedValueOnce();

const result: Result<string, DomainError> = await service.setPassword(userId, generatedPassword);
Expand All @@ -753,10 +751,7 @@ describe('KeycloakUserService', () => {
describe('if password is permanent', () => {
it('should return result with ok:true and new permanent password', async () => {
const userId: string = faker.string.numeric();
const generatedPassword: string = faker.string.alphanumeric({
length: { min: 10, max: 10 },
casing: 'mixed',
});
const generatedPassword: string = generatePassword();
kcUsersMock.resetPassword.mockResolvedValueOnce();

const result: Result<string, DomainError> = await service.setPassword(
Expand All @@ -783,10 +778,7 @@ describe('KeycloakUserService', () => {
describe('when error is thrown during password-reset', () => {
it('should pass error', async () => {
const userId: string = faker.string.numeric();
const generatedPassword: string = faker.string.alphanumeric({
length: { min: 10, max: 10 },
casing: 'mixed',
});
const generatedPassword: string = generatePassword();
kcUsersMock.resetPassword.mockRejectedValueOnce(new Error());
const result: Result<string, DomainError> = await service.setPassword(userId, generatedPassword);
expect(result).toStrictEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { Person } from '../../person/domain/person.js';
import { DomainError } from '../../../shared/error/domain.error.js';
import { PersonFactory } from '../../person/domain/person.factory.js';
import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('Organisation API', () => {
let app: INestApplication;
Expand Down Expand Up @@ -166,7 +167,7 @@ describe('Organisation API', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personData instanceof DomainError) {
throw personData;
Expand Down
7 changes: 2 additions & 5 deletions src/modules/person/domain/person.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { faker } from '@faker-js/faker';
import { DomainError, MismatchedRevisionError } from '../../../shared/error/index.js';
import { Geschlecht, Vertrauensstufe } from './person.enums.js';
import { UsernameGeneratorService } from './username-generator.service.js';
Expand All @@ -7,6 +6,7 @@ import { VornameForPersonWithTrailingSpaceError } from './vorname-with-trailing-
import { FamiliennameForPersonWithTrailingSpaceError } from './familienname-with-trailing-space.error.js';
import { PersonalNummerForPersonWithTrailingSpaceError } from './personalnummer-with-trailing-space.error.js';
import { UserLock } from '../../keycloak-administration/domain/user-lock.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

type PasswordInternalState = { passwordInternal: string | undefined; isTemporary: boolean };

Expand Down Expand Up @@ -295,9 +295,6 @@ export class Person<WasPersisted extends boolean> {
}

public resetPassword(): void {
this.passwordInternalState.passwordInternal = faker.string.alphanumeric({
length: { min: 10, max: 10 },
casing: 'mixed',
});
this.passwordInternalState.passwordInternal = generatePassword();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { PersonFactory } from '../../person/domain/person.factory.js';
import { PersonenkontextCreationService } from '../domain/personenkontext-creation.service.js';
import { DuplicatePersonalnummerError } from '../../../shared/error/duplicate-personalnummer.error.js';
import { PersonenkontexteUpdateError } from '../domain/error/personenkontexte-update.error.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

function createRolle(this: void, rolleFactory: RolleFactory, params: Partial<Rolle<boolean>> = {}): Rolle<false> {
const rolle: Rolle<false> | DomainError = rolleFactory.createNew(
Expand Down Expand Up @@ -130,7 +131,7 @@ describe('DbiamPersonenkontextWorkflowController Integration Test', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personResult instanceof DomainError) {
throw personResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { Organisation } from '../../organisation/domain/organisation.js';
import { PersonFactory } from '../../person/domain/person.factory.js';
import { UsernameGeneratorService } from '../../person/domain/username-generator.service.js';
import { PersonenkontextMigrationRuntype } from '../domain/personenkontext.enums.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('dbiam Personenkontext API', () => {
let app: INestApplication;
Expand Down Expand Up @@ -105,7 +106,7 @@ describe('dbiam Personenkontext API', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personResult instanceof DomainError) {
throw personResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
createAndPersistRootOrganisation,
} from '../../../../test/utils/organisation-test-helper.js';
import { UserLockRepository } from '../../keycloak-administration/repository/user-lock.repository.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('dbiam Personenkontext Repo', () => {
let module: TestingModule;
Expand Down Expand Up @@ -138,7 +139,7 @@ describe('dbiam Personenkontext Repo', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personResult instanceof DomainError) {
throw personResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { KeycloakUserService } from '../../keycloak-administration/index.js';
import { ServiceProviderRepo } from '../../service-provider/repo/service-provider.repo.js';
import { DBiamPersonenkontextRepoInternal } from './internal-dbiam-personenkontext.repo.js';
import { UserLockRepository } from '../../keycloak-administration/repository/user-lock.repository.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('dbiam Personenkontext Repo', () => {
let module: TestingModule;
Expand Down Expand Up @@ -115,7 +116,7 @@ describe('dbiam Personenkontext Repo', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personResult instanceof DomainError) {
throw personResult;
Expand Down
5 changes: 3 additions & 2 deletions src/modules/rolle/api/rolle.controller.integration-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { DomainError } from '../../../shared/error/domain.error.js';
import { PersonFactory } from '../../person/domain/person.factory.js';
import { KeycloakConfigModule } from '../../keycloak-administration/keycloak-config.module.js';
import { RolleServiceProviderBodyParams } from './rolle-service-provider.body.params.js';
import { generatePassword } from '../../../shared/util/password-generator.js';

describe('Rolle API', () => {
let app: INestApplication;
Expand Down Expand Up @@ -715,7 +716,7 @@ describe('Rolle API', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personData instanceof DomainError) {
throw personData;
Expand Down Expand Up @@ -817,7 +818,7 @@ describe('Rolle API', () => {
vorname: faker.person.firstName(),
familienname: faker.person.lastName(),
username: faker.internet.userName(),
password: faker.string.alphanumeric(8),
password: generatePassword(),
});
if (personData instanceof DomainError) {
throw personData;
Expand Down
29 changes: 29 additions & 0 deletions src/shared/util/password-generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { generatePassword } from './password-generator.js';

describe('passwordGenerator', () => {
it('should return passwords with the correct length', () => {
for (let i: number = 4; i < 50; i++) {
expect(generatePassword(i)).toHaveLength(i);
}
});

it('should always return passwords with at least 4 characters', () => {
for (let i: number = 0; i < 4; i++) {
expect(generatePassword(i)).toHaveLength(4);
}
});

it.each([
{ name: 'lowercase', regex: /[abcdefghijklmnopqrstuvwxyz]/ },
{ name: 'uppercase', regex: /[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/ },
{ name: 'numbers', regex: /[0123456789]/ },
{ name: 'symbols', regex: /[+\-*/%&!?@$#]/ },
])('Should contain $name', ({ regex }: { regex: RegExp }) => {
// Repeat the test 100 times
for (let i: number = 0; i < 100; i++) {
const password: string = generatePassword(4);

expect(password).toEqual(expect.stringMatching(regex));
}
});
});
35 changes: 35 additions & 0 deletions src/shared/util/password-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { randomInt } from 'node:crypto';

const LOWERCASE: string = 'abcdefghijklmnopqrstuvwxyz';
const UPPERCASE: string = LOWERCASE.toUpperCase();
const NUMBERS: string = '0123456789';
const SYMBOLS: string = '+-*/%&!?@$#';
const ALL_CHARACTERS: string = LOWERCASE + UPPERCASE + NUMBERS + SYMBOLS;

function randomChar(str: string): string {
return str.charAt(randomInt(str.length));
}

/**
* Generates a random password with the following rules:
* - At least one lowercase character
* - At least one uppercase character
* - At least one number
* - At least one symbol
*
* Passwords will always be at least 4 characters long, to fulfill these rules
* @param length The length of the password
*/
export function generatePassword(length: number = 8): string {
let password: string = '';
password += randomChar(LOWERCASE); // One lowercase char
password += randomChar(UPPERCASE); // One uppercase char
password += randomChar(NUMBERS); // One number
password += randomChar(SYMBOLS); // One symbol

for (let i: number = password.length; i < length; i++) {
password += randomChar(ALL_CHARACTERS); // Fill the rest with random characters
}

return password;
}

0 comments on commit c5d17ad

Please sign in to comment.