Skip to content
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

[AIDANT][REINITIALISATION MOT DE PASSE] Valide le lien de réinitialisation du mot de passe #733

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions mon-aide-cyber-api/src/authentification/ServiceUtilisateur.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { EntrepotUtilisateur } from './Utilisateur';
import { FournisseurHorloge } from '../infrastructure/horloge/FournisseurHorloge';
import { differenceInMinutes } from 'date-fns';
import { ErreurMAC } from '../domaine/erreurMAC';
import { sommeDeControle } from './sommeDeControle';

type Token = {
identifiant: string;
date: Date;
sommeDeControle: string;
};

type ModificationMotDePasse = {
Expand All @@ -10,17 +16,45 @@ type ModificationMotDePasse = {
token: Token;
};

export class ErreurReinitialisationMotDePasse extends Error {}

const VINGT_MINUTES = 20;

export class ServiceUtilisateur {
constructor(private readonly entrepotUtilisateur: EntrepotUtilisateur) {}

modifieMotDePasse(
modificationMotDePasse: ModificationMotDePasse
): Promise<void> {
const lapsDeTemps = differenceInMinutes(
FournisseurHorloge.maintenant(),
modificationMotDePasse.token.date
);
if (isNaN(lapsDeTemps) || lapsDeTemps > VINGT_MINUTES) {
return this.rejette();
}
return this.entrepotUtilisateur
.lis(modificationMotDePasse.token.identifiant)
.then((utilisateur) => {
if (
sommeDeControle(utilisateur.motDePasse) !==
modificationMotDePasse.token.sommeDeControle
) {
return this.rejette();
}
utilisateur.motDePasse = modificationMotDePasse.motDePasse;
return this.entrepotUtilisateur.persiste(utilisateur);
});
}

private rejette() {
return Promise.reject(
ErreurMAC.cree(
'Réinitialisation mot de passe',
new ErreurReinitialisationMotDePasse(
'Le lien de réinitialisation du mot de passe n’est plus valide.'
)
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AdaptateurEnvoiMail } from '../../adaptateurs/AdaptateurEnvoiMail';
import { ServiceDeChiffrement } from '../../securite/ServiceDeChiffrement';
import { adaptateurCorpsMessage } from './adaptateurCorpsMessage';
import { adaptateurEnvironnement } from '../../adaptateurs/adaptateurEnvironnement';
import { FournisseurHorloge } from '../../infrastructure/horloge/FournisseurHorloge';
import { sommeDeControle } from '../sommeDeControle';

export type CommandeReinitialisationMotDePasse = Commande & {
type: 'CommandeReinitialisationMotDePasse';
Expand All @@ -28,7 +30,11 @@ export class CapteurCommandeReinitialisationMotDePasse
.then((utilisateur) => {
const partieChiffree = this.serviceDeChiffrement.chiffre(
Buffer.from(
JSON.stringify({ identifiant: utilisateur.identifiant }),
JSON.stringify({
identifiant: utilisateur.identifiant,
date: FournisseurHorloge.maintenant(),
sommeDeControle: sommeDeControle(utilisateur.motDePasse),
}),
'binary'
).toString('base64')
);
Expand Down
4 changes: 4 additions & 0 deletions mon-aide-cyber-api/src/authentification/sommeDeControle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import crypto from 'crypto';

export const sommeDeControle = (valeur: string) =>
crypto.createHash('sha256').update(valeur).digest('base64');
3 changes: 2 additions & 1 deletion mon-aide-cyber-api/src/domaine/erreurMAC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export type Contexte =
| 'Modifie le mot de passe'
| 'Modifie le profil Aidant'
| 'Modifie les préférences de l’Aidant'
| "Recherche d'un Aidé";
| "Recherche d'un Aidé"
| 'Réinitialisation mot de passe';

export class ErreurMAC<T extends Error> extends Error {
private constructor(
Expand Down
8 changes: 8 additions & 0 deletions mon-aide-cyber-api/test/api/routesAPIUtilisateur.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { AdaptateurDeVerificationDeSessionDeTest } from '../adaptateurs/Adaptate
import { unUtilisateur } from '../constructeurs/constructeursAidantUtilisateur';
import { ReponseReinitialisationMotDePasseEnErreur } from '../../src/api/routesAPIUtilisateur';
import crypto from 'crypto';
import { FournisseurHorloge } from '../../src/infrastructure/horloge/FournisseurHorloge';
import { FournisseurHorlogeDeTest } from '../infrastructure/horloge/FournisseurHorlogeDeTest';

describe('le serveur MAC sur les routes /api/utilisateur', () => {
const testeurMAC = testeurIntegration();
Expand Down Expand Up @@ -171,13 +173,19 @@ describe('le serveur MAC sur les routes /api/utilisateur', () => {

describe('Quand une requête PATCH est reçue sur /api/utilisateur/reinitialiser-mot-de-passe', () => {
it('Modifie le mot de passe', async () => {
FournisseurHorlogeDeTest.initialise(new Date());
const utilisateur = unUtilisateur()
.avecUnMotDePasse('original')
.construis();
await testeurMAC.entrepots.utilisateurs().persiste(utilisateur);
const token = btoa(
JSON.stringify({
identifiant: utilisateur.identifiant,
date: FournisseurHorloge.maintenant(),
sommeDeControle: crypto
.createHash('sha256')
.update(utilisateur.motDePasse)
.digest('base64'),
})
);

Expand Down
105 changes: 105 additions & 0 deletions mon-aide-cyber-api/test/authentification/ServiceUtilisateur.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, it } from 'vitest';
import { FournisseurHorloge } from '../../src/infrastructure/horloge/FournisseurHorloge';
import { FournisseurHorlogeDeTest } from '../infrastructure/horloge/FournisseurHorlogeDeTest';
import {
ErreurReinitialisationMotDePasse,
ServiceUtilisateur,
} from '../../src/authentification/ServiceUtilisateur';
import { EntrepotUtilisateurMemoire } from '../../src/infrastructure/entrepots/memoire/EntrepotMemoire';
import crypto from 'crypto';
import { add } from 'date-fns';
import { ErreurMAC } from '../../src/domaine/erreurMAC';
import { unUtilisateur } from '../constructeurs/constructeursAidantUtilisateur';
import { sommeDeControle } from '../../src/authentification/sommeDeControle';

describe('Service utilisateur', () => {
describe('Modification du mot de passe', () => {
it('N’est valide que pendant 20 minutes', async () => {
FournisseurHorlogeDeTest.initialise(new Date());
const dateGenerationToken = FournisseurHorloge.maintenant();
FournisseurHorlogeDeTest.initialise(
add(FournisseurHorloge.maintenant(), { minutes: 21 })
);

expect(
new ServiceUtilisateur(
new EntrepotUtilisateurMemoire()
).modifieMotDePasse({
motDePasse: 'mdp',
confirmationMotDePasse: 'mdp',
token: {
identifiant: crypto.randomUUID(),
date: dateGenerationToken,
sommeDeControle: '',
},
})
).rejects.toStrictEqual(
ErreurMAC.cree(
'Réinitialisation mot de passe',
new ErreurReinitialisationMotDePasse(
'Le lien de réinitialisation du mot de passe n’est plus valide.'
)
)
);
});

it('N’est valide qu’une seule et unique fois', async () => {
FournisseurHorlogeDeTest.initialise(new Date());
const utilisateur = unUtilisateur()
.avecUnMotDePasse('nouveau-mot-de-passe')
.construis();
const entrepotUtilisateur = new EntrepotUtilisateurMemoire();
await entrepotUtilisateur.persiste(utilisateur);

expect(
new ServiceUtilisateur(entrepotUtilisateur).modifieMotDePasse({
motDePasse: 'mdp',
confirmationMotDePasse: 'mdp',
token: {
identifiant: utilisateur.identifiant,
date: FournisseurHorloge.maintenant(),
sommeDeControle: crypto
.createHash('sha256')
.update('ancien-mot-de-passe')
.digest('base64'),
},
})
).rejects.toStrictEqual(
ErreurMAC.cree(
'Réinitialisation mot de passe',
new ErreurReinitialisationMotDePasse(
'Le lien de réinitialisation du mot de passe n’est plus valide.'
)
)
);
});

it('N’est pas valide si la date est invalide', async () => {
FournisseurHorlogeDeTest.initialise(new Date());
FournisseurHorlogeDeTest.initialise(
add(FournisseurHorloge.maintenant(), { minutes: 21 })
);

expect(
new ServiceUtilisateur(
new EntrepotUtilisateurMemoire()
).modifieMotDePasse({
motDePasse: 'mdp',
confirmationMotDePasse: 'mdp',
token: {
identifiant: crypto.randomUUID(),
date: null as unknown as Date,
sommeDeControle: sommeDeControle('ancien'),
},
})
).rejects.toStrictEqual(
ErreurMAC.cree(
'Réinitialisation mot de passe',
new ErreurReinitialisationMotDePasse(
'Le lien de réinitialisation du mot de passe n’est plus valide.'
)
)
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { unUtilisateur } from '../../constructeurs/constructeursAidantUtilisateu
import { CapteurCommandeReinitialisationMotDePasse } from '../../../src/authentification/reinitialisation-mot-de-passe/CapteurCommandeReinitialisationMotDePasse';
import { FauxServiceDeChiffrement } from '../../infrastructure/securite/FauxServiceDeChiffrement';
import { adaptateurCorpsMessage } from '../../../src/authentification/reinitialisation-mot-de-passe/adaptateurCorpsMessage';
import { FournisseurHorloge } from '../../../src/infrastructure/horloge/FournisseurHorloge';
import { FournisseurHorlogeDeTest } from '../../infrastructure/horloge/FournisseurHorlogeDeTest';
import { sommeDeControle } from '../../../src/authentification/sommeDeControle';

describe('Capteur de commande de réinitialisation du mot de passe', () => {
beforeEach(() => {
process.env.URL_MAC = 'http://localhost:8081';
});
it('Envoie le mail de modification de mot de passe', async () => {
FournisseurHorlogeDeTest.initialise(new Date());
const entrepots = new EntrepotsMemoire();
const busEvenement = new BusEvenementDeTest();
const adaptateurEnvoiMail = new AdaptateurEnvoiMailMemoire();
Expand All @@ -32,6 +36,8 @@ describe('Capteur de commande de réinitialisation du mot de passe', () => {
Buffer.from(
JSON.stringify({
identifiant: utilisateur.identifiant,
date: FournisseurHorloge.maintenant(),
sommeDeControle: sommeDeControle(utilisateur.motDePasse),
}),
'binary'
).toString('base64'),
Expand Down
Loading