Bonjour à tous

Je gère le site d’une association où il y a une partie privée réservée aux membres de l’association. C’est l’administrateur du site qui inscrit les nouveaux membres. Un message est envoyé automatiquement aux nouveaux membres avec un mot de passe temporaire généré aléatoirement.
A l’époque où cela a été créé (il y a une vingtaine d’années) ça ne choquait personne de voir circuler un mot de passe (temporaire ou non) en clair par courriel.

Je voudrais votre avis sur la méthode à employer à savoir:
1 - conserver la méthode actuelle en renforçant le contrôle sur ce que peuvent accéder les membres tant qu’ils n’ont pas changé leur mot de passe
2 - envoyer un message d’inscription avec un lien vers une page où ils peuvent déclarer leur mot de passe, sachant que le mot de passe n’est de toute façon pas stocké en clair sur le site
3 - tout autre mécanisme dont vous auriez l’expérience dans une situation similaire

Merci de bien vouloir partager votre expérience
Modérateur
Bonjour,

L'on voit aujourd'hui plutôt un lien avec une valeur générée aléatoirement et une validité de quelques heures (12/24/48/...) qui accueil le nouvel inscrit sur la page de config de son profile où il doit au moins indiquer et enregistrer un mot de passe + d'autres infos essentielles selon ..) avant qu'il puisse accéder à la partie privée (en repassant éventuellement par un lien de validation , envoyer sur son mail , si besoin et pas trop barbant pour l'utilisateur.)

Cela évite un mot de passe en clair, mais donne quand même un lien éphémère.
On peut en profiter pour placer un mot sympa et y expliquer le fonctionnement du site, regle de conduite, etc...

Cdt
Modifié par gcyrillus (23 Sep 2021 - 14:35)
Merci de ta réponse
C'est en gros ce à quoi je pensais pour la solution 2.

Tu peux de donner un exemple de message à envoyer avec le lien temporaire?

Ce que j'ai essayé de faire ne me parait pas suffisamment sexy:

<div style="margin:6pt 0;max-width:600px;">
    <p>Bonjour et bienvenue %firstName%</p>
    <p>Vous êtes inscrit dans la liste des personnes ayant accès à la partie privée du site Alma Musica</p>
    <p>Pour compléter cette inscription, cliquez sur le lien ci-dessous.</p>
    <a  style="display:table;margin:4pt auto;
 	background-color:gold;color:black;font-weigth:bold;text-align:center;
	border-radius:6pt;padding:6pt;"				
	href="https://www.alma-musica.net/inscription.php?code=%renewCode%">Complétez mon inscription</a>
</div>

Par expérience, j'évite de mettre une feuille de style dans les mails, de nombreux outils de messagerie ne les supportent pas.
Modifié par PapyJP (23 Sep 2021 - 15:37)
Modérateur
De mon côté, je gère comme ceci :
- formulaire d'inscription avec mot de passe et confirmation de mot de passe. (8 caractères minimum : majuscule/minuscule, chiffre, caractères "spéciaux", pas de mot courant (!). Le hashage se fait en sha256 avec clef de sécurité
- confirmation inscription
- mécanique formulaire login et redirection logout
- formulaire mot de passe oublié
- confirmation reset password (mail envoyé)

*Tous les formulaires ont un token anti csrf
Modifié par niuxe (23 Sep 2021 - 17:57)
Si je comprends bien tunes dans le mode "l’utilisateur s’inscrit lui-même", quitte à ce que l’administrateur le supprime. Dans mon cas c’est l’administrateur qui inscrit les utilisateurs. On doit donc envoyer un lien à l’utilisateur pour qu’il entre son mot de passe. Dans l’état actuel de la discussion je pense effectivement que je vais envoyer un message sans mot de passe. Le problème maintenant est de rédiger un message de bienvenue sexy.
Bonjour,

Tu peux utiliser le système proposé par niuxe en conservant la gestion des utilisateurs dans l'administration du site.
1 / Tu ajoutes un champ token (nullable) dans ta table des utilisateurs,
2 / Quand tu crées un utilisateur, un token (unique, avec durée de validité, etc.) lui est généré et un mail avec un lien de validation contenant le token lui est envoyé
3 / Quand ton user clique sur le lien, il entre son e-mail et peut choisir son mot de passe, le token de l'utilisateur est remis à 'null' et le lien n'est plus utilisable.

ça pourra te permettre par la suite de garder le même système pour la fonctionnalité "mot de passe perdu".
Merci, c'est ce que je suis en train de coder.

J'ai découvert que le mot de passe crypté utilisé par Apache sur mon site est correctement obtenu par la formule

$cryptPW = '$apr1$' . crypt($pw, base64_encode($pw))

Je ne me souviens plus comment j'ai trouvé ça, mais bon! ça marche.
Je vais donc prendre le mot de passe fourni par l'utilisateur et mettre dans le fichier de contrôle la valeur retournée par cette fonction.

Question:
Dans le code actuel, j'ai une fonction "changer de mot de passe" dans laquelle je demande à l'utilisateur
- son mot de passe actuel
- son nouveau mot de passe
- retaper son nouveau mot de passe

Si je voulais faire la même chose dans le nouveau code, où je ne dispose plus du mot de passe en clair, il faudrait que je puisse vérifier que le mot de passe actuel est cohérent avec le mot de passe crypté calculé par l'algorithme ci-dessus.

Je trouve plein de choses dans la doc pour vérifier la validité d'un mot de passe, mais ça n'a pas l'air de correspondre à ce que je fais pour Apache.

Savez vous comment faire ça, l'alternative étant de remplacer la fonction "changer de mot de passe" par "j'ai perdu mon mot de passe", c'est à dire sans vérification initiale du mot de passe actuel ?
Modérateur
Bonsoir,

Attention, base64_encode() ne crypte pas, il encode et base64_decode() te redonne en clair ce qui est encoder. Cela peut te servir a cacher visuellement une URL, mais pour un mot de passe ça revient à le stocké en clair.

Pour crypter en php tu as plusieurs pistes :

https://www.php.net/manual/fr/book.password.php / https://www.php.net/manual/fr/faq.passwords.php
https://www.php.net/manual/en/function.hash.php
https://www.php.net/manual/fr/function.sha1.php
https://www.php.net/manual/fr/function.md5.php
_____________________________________________________________
https://www.php.net/manual/fr/function.base64-encode.php
https://www.php.net/manual/fr/function.base64-decode.php
il y a aussi https://www.php.net/manual/fr/function.urlencode.php

et puis tu peux aussi générer "un grain de sel" pour rendre le plus unique possible cette chaine de caractère que tu veut crypter.

Il existe des dictionnaires de mots et leurs équivalent cryptés qui ne cessent de s'agrandir au fil du temps.
Ce qui fait qu'en utilisant une méthode seule, une chaine de caractères retournera toujours la même chaine cryptée. En lançant en boucle un programme qui génère des chaines de mots ou caractères aléatoires qui sont stockée avec leur équivalent crypté, le supermotdepassekamoi finira par apparaitre dans ce foutu dico.

L'ajout d'une chaine de caractères aléatoires (l’assaisonnement) à la chaine crypté rend la probabilité de voir ce mot figuré au dico s’amoindrir.

Tu peut par exemple faire un mix : ajouté un grain de sel a la chaine a cryptée et imbriqué 2 modes de cryptage moyen pour obtenir un cryptage suffisamment costaud.

En faisant cela, il te faudra stocké le grain de sel et la chaine crypté.
Pour vérifier la chaine , il faut réutiliser son grain de sel et le cryptage et comparé ce résultat avec la chaine cryptée déjà stockée .

En cas de mot de passe perdu, il est impossible de retrouvé le mot de passe, il faut en généré un nouveau. Ce lien temporaire envoyé sur une adresse mail , donnée au moment de l'inscription ou figurant dans les données de l'utilisateur peut-être encode en base64 pour y cacher quelque données utiles à reconnaitre le membre et une valeur cryptée de référence , ou bien seule la valeur cryptée enregistrée dans les données du membre que l'on recherchera en bdd.

Voici un exemple (A NE PAS UTILISER TEL QUEL) qui utilise encode64()/decode64() pour le lien d'activation/récupération qui sera visible encodé dans le mail mais au final en clair , et la génération d'un grain de sel ajouter à une date cryptée en imbriquant sha1( 'grain_de_sel'. md5('la_date_a_cacher')) . Ce n'est pas incraquable, mais les ressources à mettre en œuvre n'en valent vraiment pas le coup, c'est pas les coffres de forknox Smiley smile .

Voici donc l'exemple qui se base sur un fichier texte (au lieu d'une bdd) pour une démo.
! Ce n'est qu'une démo sans bonnes pratiques, juste pour montrer l'idée évoquée précédemment à propos d'un lien temporaire , je crois que ça couvre pas trop mal un déroulé possible.
<?php  // NE pas utiliser en vrai - methode obsolete et codage à l'arrache, c'est juste une démo d'exemple / voir  https://www.php.net/manual/fr/function.password-verify.php  une fois l'intêret du stockage d'un mot passe crypté
if(isset($_GET['monCompte'])) {
	$seeyou = base64_decode($_GET['monCompte']);
	header("location:   http://".$_SERVER['SERVER_NAME'].$_SERVER['PHP_SELF'].$seeyou  );
}
?>
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>  test</title>
  <meta name="description" content=" test valid connexion ">
  <meta name="author" content="MyPc">
  <link rel="stylesheet" href="css/styles.css?v=1.0">
</head>
<style>
body{display:grid;justify-content:center;}
</style>
<body>
	<h1 style="width:max-content;margin:1em auto;max-width:80%;">test generation lien connexion temporaire</h1>
<?php

if(isset($_POST['pwd1'])) {
	$mdp = $_POST['pwd1'];
	$mdp64         = base64_encode($mdp);
	$mdpdec64      = base64_decode($mdp64);
	
	$mdp_md5        = md5($mdp);
	
	$mdp_sha1       = sha1($mdp);
	
	$clef_de_sel   = 'UneChaineAuHasard'; // soit generation aléatoire ou par exemple prit dans les infos fournit par le membre (date d'inscription,pseudo,nbre de connexions,n] d'enregistrement , mail, etc  selon votre imagination)
	
	
	$mdp64_sel         = base64_encode($mdp.$clef_de_sel);
	$mdpdec64_sel      =str_replace($clef_de_sel, '',base64_decode( $mdp64_sel));
	
	
	$mdp_md5_sel    = md5($mdp.$clef_de_sel);
	
	$mdp_sha1_sel   = sha1($mdp.$clef_de_sel);	
	
	$mdp_sha1_md5_sel =sha1(md5($mdp).$clef_de_sel); // ou sha1($clef_de_sel.md5($mdp)) ou sha1($clef_de_sel.md5($mdp$clef_de_sel.)) ....
	
	$mdp_md5_sha1_sel =md5(sha1($mdp).$clef_de_sel); // ou md5($clef_de_sel.sha1($mdp)) ou md5($clef_de_sel.sha1($mdp$clef_de_sel.)) ....
	echo '<style>table,td ,th {border:solid 1px;text-align:center;padding:0.15em;} th {background:lightgray}.oups{color:crimson;background:yellow}</style>
	<p><b>Il n\'y a rien eu d\'enregistrer durant cette étape de l\'exemple</b>, mais voici quelques possibilités , <b>pour sécuriser un mot de passe il ne faut pas utiliser une méthode reversible</b>.</p>
	<table>
		<thead>
			<tr><th>   mot de passe</th>     <th> hachoir                       </th><th>chaine modifiée à stockée</th><th> decodage natif  disponible                       </th><th>résultat          </th><th>grain de sel                  </th></tr>
		</thead>
		<tbody>
			<tr><th rowspan="8">'.$mdp.'</td><td>base64_encode("pwd")           </td><td>'.$mdp64.'               </td><td> base64_decode()                                  </td><td class="oups">'.$mdpdec64.'     </td><td> non                          </td></tr>
			<tr>                             <td>base64_encode("pwd + sel" )    </td><td>'.$mdp64_sel.'           </td><td> str_replace(sel, "",base64_decode(chainee salée))</td><td class="oups">'.$mdpdec64_sel.' </td><td rowspan="  ">'.$clef_de_sel.' </td></tr>
			<tr>                             <td>md5("pwd")                     </td><td>'.$mdp_md5.'             </td><td>  non                                             </td><td>aucun sans hachage             </td><td rowspan="  ">non              </td></tr>
			<tr>                             <td>sha1("pwd")                    </td><td>'.$mdp_sha1.'            </td><td>  non                                             </td><td>aucun sans hachage             </td><td rowspan=" 5 ">'.$clef_de_sel.'</td></tr>
			<tr>                             <td>md5("pwd + sel")               </td><td>'.$mdp_md5_sel.'         </td><td>  non                                             </td><td>aucun sans hachage             </td>                                       </tr>
			<tr>                             <td>sha1("pwd + sel")              </td><td>'.$mdp_sha1_sel.'        </td><td>  non                                             </td><td>aucun sans hachage             </td>                                       </tr>
			<tr>                             <td>sha1(md5("pwd + sel")          </td><td>'.$mdp_sha1_md5_sel.'    </td><td>  non                                             </td><td>aucun sans hachage             </td>                                       </tr>
			<tr>                             <td>md5(sha1("pwd + sel")          </td><td>'.$mdp_md5_sha1_sel.'    </td><td>  non                                             </td><td>aucun sans hachage             </td>                                       </tr>
		</tbody>
	</table>
	<p>Si grain de sel, il vaut mieux que celui-ci soit unique à chaque enregistrement , il faut donc le stocker avec la chaine modifié</p>
	<p><a href="http://'.$_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'].'">Retour au formulaire</a></p>
</body>
</html>';
exit;
}


$date=date("Y-m-d");
$duree ="+2day";
if(isset($_POST['duree'])){$duree=$_POST['duree'];}
$lastValidDate = strtotime($duree, strtotime($date));

if(isset($_POST['name'])) { 
 echo '<p>demande enregistrée en attente d\'activation</p>';		
	$salt = '';
	$chaine = 'abcdefghijklmnpqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
	mt_srand((float)microtime()*1000000);
	
		for($i=0; $i<10; $i++) $salt .= $chaine[ mt_rand()%strlen($chaine) ];
		
	$limit=sha1($salt.md5($lastValidDate));
	
	
	$filelimit=$date;
	$nom =$_POST['name'];
	$prenom =$_POST['nickname'];
	
	$fileMember= $prenom.'.'.$nom.'.quand';
	$fileMemberContent= $salt.','.$date .','.$duree;
	
	$open = fopen($fileMember, "w") ;
	fwrite($open, $fileMemberContent);
	fclose($open);
	
	$lien = '?p='.$nom.'&pr='.$prenom.'&val='.$limit ;
	$secret=  base64_encode($lien);
	
	$lienValidationEnAttente = $_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'].'?monCompte='.$secret  ;
				
	echo '<p> Pour finaliser votre inscription veuillez créer votre mot de passe en suivant ce lien:<br> <a href="http://'.$lienValidationEnAttente.'">Activer mon espace personnel</a>.</p>';
	
	
	
}


if(isset($_GET['p']) && isset($_GET['pr']) && isset($_GET['val']) ) {
	echo '<p>Bienvenue <b style="text-transform:uppercase">'.$_GET['pr'].' '.$_GET['p'].'</b>.</p>';

	if(@$open = fopen($_GET['pr'].'.'.$_GET['p'].'.quand', "r")) {
    while (($data = fgetcsv($open, 100, ",")) !== FALSE)     {        
      $array[] = $data; 
    }  
    fclose($open);
	
		foreach($array as $i => $line){ 
		 $end = strtotime($line[2], strtotime($line[1]));
		 $check=sha1($line[0].md5($end)); 
		 //verif de la date de validité
			if ($check == $_GET['val']) {
				// la date est elle dépasée ?
				$dnow = strtotime($date);
				if (($dnow < $end) || ($dnow == $end)) {
					echo '<form action="" method="post"  style="display:grid;justify-content:center;">
					<fieldset>
						<legend style="text-transform:uppercase">Mise à jour du mot de passe</legend>
							<div style="display:grid;grid-template-columns:auto auto;gap:0.5em;justify-content:center;">
								<label>Votre nouveau mot de passe</label>
								<input name="pwd1">
								<label>Retaper votre mot de passe</label>
								<input name="pwd2">
								<input type="submit">
							</div>
						</fieldset>
					</form>';
					unlink($_GET['pr'].'.'.$_GET['p'].'.quand');
					}
					else { echo 'Ce lien a expiré, veuillez renouveler votre demande d\'inscription';}
				
			}
		}
	} 	else { 		echo '<p style="color:orange">Il n`y a actuellement aucune demande au nom de <b style="text-transform:uppercase"> '.$_GET['pr'].' '.$_GET['p'].' </b> en attente de finalisation. </p>';	}
}

?>
<?php if((empty($_POST)) && (empty($_GET))) {?>
	<form action="" method="post"  style="display:grid;justify-content:center;">
		<fieldset>
			<legend style="text-transform:uppercase">Demande d'nscription</legend>
			<div style="display:grid;grid-template-columns:auto auto;gap:0.5em;justify-content:center;">
			<label for="name">Nom</label>
			<input type="text" name="name">
			<label for="nickname">Prénom</label>
			<input type="text" name="nickname">
			<label for="mail">Courriel</label>
			<input type="email" name="mail">
			<label for="duree">validité</label>
			<select name="duree">
				<option value="-1 month">test date expirée</option>
				<option value="+1 day">1 journée</option>
				<option value="+2 day" selected>2 jours</option>
				<option value="+10 day">10 jours</option>
				<option value="+1 month">1 mois</option>
				<option value="+1 year">1 ans</option>
			</select>
			<input type="submit">
			</div>
		</fieldset>
	</form>
<?php } ?>
	<p><a href="http://<?php echo $_SERVER['SERVER_NAME'].$_SERVER['SCRIPT_NAME'].'">Retour au formulaire</a></p>'; ?>
</body>
</html>


Cdt
Modifié par gcyrillus (25 Sep 2021 - 14:25)
Merci de ta réponse
Il y a quelque chose que je ne comprends pas: une fois que l’utilisateur a donné son mot de passe, on en fait quoi?
Actuellement je transforme le mot de passe par la fonction expliquée plus haut et je mets dans le fichier de contrôle une ligne "pseudo:mot-de-passe-transformé" dans le fichier de contrôle.
Je suppose que je vais devoir faire la même chose, ce qui me va très bien.
La fonction de transformation que j’ai trouvée je ne sais où donne un résultat qui convient à Apache, c’est le principal. Je suis d’accord que c’est faible comme protection, mais je n’ai pas pour le moment connaissance d’une meilleure fonction de transformation.
Par ailleurs je ne tiens pas à demander aux inscrits actuels de renouveler leur mot de passe parce que j’ai changé mon programme.
Modérateur
PapyJP a écrit :
Merci de ta réponse
Il y a quelque chose que je ne comprends pas: une fois que l’utilisateur a donné son mot de passe, on en fait quoi?
.

Tu l'encrypte et tu stocke cette nouvelle chaine illisible. Ce mot de passe ne sera connu que de l'utilisateur.
pour se loguer, l'utilisateur tape son mot de passe, il est récupéré et cryptée (même méthode qu'à l'enregistrement), la chaine illisible alors produite est comparée à celle qui est stockée. Elles seront identiques si le mot de passe est le bon (le cryptage n'est pas aléatoire).

L'exemple utilise un grain de sel, lui aléatoire mais, conservé pour être réutilisé . utilité: même si deux chaines sont identiques, avec le grain de sel, ce ne sera plus le cas. (crypté ou pas).

PapyJP a écrit :

Actuellement je transforme le mot de passe par la fonction expliquée plus haut et je mets dans le fichier de contrôle une ligne "pseudo:mot-de-passe-transformé" dans le fichier de contrôle.
Je suppose que je vais devoir faire la même chose, ce qui me va très bien.
La fonction de transformation que j’ai trouvée je ne sais où donne un résultat qui convient à Apache, c’est le principal. Je suis d’accord que c’est faible comme protection, mais je n’ai pas pour le moment connaissance d’une meilleure fonction de transformation.

C'est toi qui choisi ta méthode Smiley cligne
PapyJP a écrit :

Par ailleurs je ne tiens pas à demander aux inscrits actuels de renouveler leur mot de passe parce que j’ai changé mon programme.


Si tes mots de passes sont actuellement stockés en clair, alors tu peut les cryptés en lot , le mot de passe à saisir sera le même. Il y aura une étape supplémentaire dans le programme pour le crypté avant de comparer les deux chaines cryptées. Si il y a fuites de données, ce sera des mots de passe cryptés , c'est principalement l'idée.

L'exemple fait mumuse en tournant en rond avec une génération de lien à durée limité et une seule utilisation, en utilisant la date du jour cryptée , cela n'a aucune incidence sur la façon dont tu stockerait le mot de passe. Mais je n'ai peut-être pas compris ton interrogation.

Dans l'exemple, la date limite est injectée dans l'url et cryptée et avec : $limit=sha1($salt.md5($lastValidDate));
et ensuite comparée a $check=sha1($line[0].md5($end)); $line['0'] c'est le grain de sel et $end la date limite . $limit et $check sont les deux chaines illisible à comparer , l'une vient de L’URL, l'autre des données stockées Smiley smile


Cdt,
GC
Mon interrogation est de faire un cryptage compatible avec ce que Apache sait décoder.

Autant que je comprenne en lisant la documentation il y a une certaine latitude dans ce domaine, mais je ne suis pas arrivé à utiliser un autre moyen de générer un mot de passe crypté utilisable par Apache que ma fameuse fonction
$crypt = '$apr1$' . crypt($pw, base64_encode($pw));

Si je comprends bien, le deuxième paramètre de la fonction crypt est le "salt", on pourrait donc mettre autre chose que l'encodage du mot passe en base64, mais pourquoi pas?
Toutes les autres façons de créer un mot de passe codé reconnu par Apache que j'ai essayées ont échoué.

Dans le cas de ce site, il n’y a pas de données réellement sensibles, la partie privée est là pour éviter que certains fichiers soient de fait en accès public à tout un chacun, par exemple si je fais un fichier musical d’une œuvre qui a été écrite il y a moins de 80 ans.
Le problème est comme toujours que les gens utilisent le même mot de passe partout, malgré les recommandations : si on trouve un mot de passe sur mon site, il pourrait être récupéré pour hacher le compte de la personne sur un autre site.
Modifié par PapyJP (25 Sep 2021 - 11:19)
Après pas mal de galère, j'ai trouvé que
$crypt = password_hash($pw, PASSWORD_DEFAULT);

avait l'air de fonctionner (pas facile à tester correctement!)
Est-ce que cela vous parait une bonne façon de faire?
Modérateur
Oui c'est ça Smiley cligne

la verif se fait en testant
password_verify('mdp', $crypt)

Modifié par gcyrillus (25 Sep 2021 - 13:57)
Effectivement mais je n’ai jamais besoin de vérifier les mots de passe, c’est Apache qui le fait.
Modérateur
C'est parfait, ton base64_encode() m'avait inquieté .
bonne fin de Week-end
Modifié par gcyrillus (25 Sep 2021 - 14:28)