Beste,
Ik ben bezig met een 2FA systeem.
Misschien kent u dat wel van Google Authenticator. Dat een gebruiker dat wil inloggen, eerst een email krijgt waar een code in staat en vervolgens die code moet invoeren om uiteindelijk in te kunnen loggen.
Nu ben ik bezig met zo'n systeem, maar ik wil geen externe API'S gebruiken. (Bijv. Google Authenticator)
In plaats daarvan wil ik een eigen systeem maken.
Maar hoe zal ik dit het best aanpakken?
Een token genereren natuurlijk, met $token = random_bytes(5).
En een verloopdatum met $expires = date(U) + 300;
Zijn er nog extra kolommen die ik moet toevoegen in de MYSQLI database, om de veiligheid van dit systeem te verbeteren, of nog andere (veiligheids)suggesties?

Alvast bedankt,
Zou dit genoeg zijn voor het genereren van een code en qua veiligheid?
<?php
// phpmailer
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require 'PHPMailer-master/src/Exception.php';
require 'PHPMailer-master/src/PHPMailer.php';
require 'PHPMailer-master/src/SMTP.php';
require 'connection.inc.php';
// variableen ophalen
$userName = $_GET['uid'];
$id = $_GET['id'];
$email2FA = $_POST['email2FA'];
$emailDB = $_GET['email'];
// klikvalidatie
if(!isset($_POST['2FAEnable'])) {
header('Location: ../adjust.php?error=invalidrequest');
exit();
}
else {
// code genereren
function randomtoken($size) {
$size = intval($size);
if($size == 0) {
return NULL;
}
$charSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$len = strlen($charSet);
$str = '';
$i = 0;
while(strlen($str) < $size) {
$num = rand(0, ($len-1));
$tmp = substr($charSet, $num, 1);
$str = $str . $tmp;
$i++;
}
return $str;
}
$token = randomtoken(6);
$expires = date("U") + 300;
// controleren of email leeg is gelaten
if(empty($email2FA)) {
header('Location: ../2FA-Enable.php?error=emptyfields&request=valid');
exit();
}
else {
// controleren of de email geldig is
if(!filter_var($email2FA, FILTER_VALIDATE_EMAIL)) {
header('Location: ../2FA-Enable.php?error=invalidemail');
exit();
} // controleren of de email geregistreerd is
else if($email2FA !== $emailDB) {
header('Location: ../2FA-Enable.php?error=emailnotregistered');
exit();
}
else {
$cookieID = setcookie('expire', $userName, time() + (86400 * 90), "/");
$hashedToken = password_hash($token, PASSWORD_DEFAULT);
$sql = 'INSERT INTO 2fa (cookieID, token, expire date) VALUES (?, ?, ?)';
$stmt = mysqli_stmt_init($conn);
if(!mysqli_stmt_prepare($stmt, $sql)) {
echo 'mysqli insert token mislukt', error_get_last();
exit();
}
else {
mysqli_stmt_bind_param($stmt, "sss", $cookieID, $hashedToken, $expires);
mysqli_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
mysqli_stmt_close($stmt);
mysqli_close($conn);
// mail verzenden met de code
$mail = new PHPMailer(true);
try {
$mail->SMTPDebug = 2;
$mail->isSMTP(); // Set mailer to use SMTP
$mail->SMTPAuth = true; // Enable SMTP authentication
$mail->SMTPSecure = 'ssl'; // Enable TLS encryption, `ssl` also accepted
$mail->Host = 'mail.axc.nl'; // Specify main and backup SMTP servers
$mail->Port = 465; // TCP port to connect to
$mail->isHTML(true); // Set email format to HTML
$mail->Username = '[email protected]'; // SMTP username
$mail->Password = 'mijn wachtwoord'; // SMTP password
//Recipients
$mail->setFrom('[email protected]');
$mail->addAddress($userEmail); // Add a recipient
$mail->addReplyTo('[email protected]', 'Ondersteuning');
//Content
$mail->Subject = '2-Staps Autorisatie jinvantongeren.nl';
$mail->Body = "<p>Er is een verzoek binnengekomen om 2-Staps Autorisatie in te voeren op uw account. \n\r Als u dit niet was, wordt u aangeraden om uw account te <a href='jinvantongeren.nl/adjust.php'>Beveiligen</a>\n\r Uw code is: ".$token."</p><br>Met vriendelijke groeten, <br> Jin van Tongeren";
$mail->AltBody = 'Er is een verzoek binnengekomen om 2-Staps Autorisatie in te voeren op uw account. \r\n. De link is: \r\n '.$url.' \r\n Met vriendelijke groeten, \r\n Jin van Tongeren ';
$mail->send();
echo '<meta http-equiv="refresh" content="0; URL=https://jinvantongeren.nl/reset-password?reset=success&request=valid">';
exit();
}
}
}
}
}
Even in het algemeen: dit klinkt toch een beetje als een deur met twee sloten waarbij de sleutels aan dezelfde sleutelbos hangen. Of mis ik iets? Ik zou persoonlijk gaan voor een verificatie met een code via SMS. Dan weet je uiteindelijk dat én de gebruiker toegang heeft tot de combinatie inlognaam (of email) en wachtwoord én dat de gebruiker ook toegang heeft tot de mobiel waar de code per sms naar toe gestuurd is.

[size=xsmall]Toevoeging op 09/03/2019 10:07:30:[/size]

@Jin: $_GET variabelen zijn nooit zeker. check voor gebruik of ze bestaan met isset().
Maar een code via de sms kost toch geld / beltegoed? Of ben ik nou mis?
Ja maar is dat niet het hele punt, dat je op een of andere manier een (tijdgevoelige) externe verificatie hebt?

Over de code hierboven: dit lijkt veel op code uit die andere thread? Wat is er op tegen om eerst alle GET/POST data te valideren en dan iemand in 1x door te sturen of terug te sturen in plaats van die geneste if-else brei? En al die foutmeldingen (?error=xyz)? Handig voor debugging wellicht, maar niet in een live systeem waarin je gedetailleerd uit de doeken doet welke informatie niet klopt.

In het algemeen is de enige terugkoppeling die je (als gebruiker) qua authenticatie zou moeten krijgen ofwel "alles ok" of "gegevens onjuist".
@thomas van den heuvel op de pagina waar ik de gebruikers naar terugstuur, zet ik dan ook een error / success melding neer.
Bijv: (dit was van een andere pagina, waar de gebruiker gegevens kon aanpassen)
<?php if(isset($_GET['error'])) {
if($_GET['error'] == 'emptyfields') {
echo '<p class="text-danger" style="text-align: center;">Vul alle velden in!</p>';
}
if($_GET['error'] == 'samedata') {
echo '<p class="text-danger" style="text-align: center;">Vul andere gegevens in!</p>';
}
else if($_GET['error'] == 'invalidName') {
echo '<p class="text-danger" style="text-align: center;">Vul een geldige naam in!</p>';
}
else if($_GET['error'] == 'invalidEmail') {
echo '<p class="text-danger" style="text-align: center;">Vul een geldige E-Mail adres in!</p>';
}
else if($_GET['error'] == 'pwdNotSame') {
echo '<p class="text-danger" style="text-align: center;">De 2 wachtwoorden komen niet overeen!</p>';
}
else if($_GET['error'] == 'fail') {
echo '<p class="text-danger" style="text-align: center;">Wijzigen mislukt. Probeer het opnieuw.</p>';
}
else if($_GET['error'] == 'pwdTooShort') {
echo '<p class="text-danger" style="text-align: center;">Wachtwoord is te kort. Minimum is 8 tekens. <a href="https://www.onlinewachtwoordgenerator.nl"; target="_blank" class="btn btn-primary">Wachtwoord generator</a></p>';
}
else if($_GET['adjust'] == 'success') {
echo '<p class="text-success" style="text-align: centwer;">Het is succesvol gewijzigd!</p>';
}
else if($_GET['error'] == 'usertaken') {
echo '<p class="text-danger" style="text-align: center;">Gebruikersnaam is al in gebruik.</p>';
}
} ?>
Ja dat snap ik, maar dat zou je dus niet moeten doen. Je geeft hiermee (ongewild) informatie prijs over je gebruikers wat uit veiligheidsoptiek niet verstandig is.
Dus ik moet de gebruikers gewoon terugsturen zonder enige informatie voor de gebruikers of ze het goed / niet goed hebben gedaan?
Gewoon enkel bij inlogfouten melden dat "het inloggen niet gelukt is". Niemand hoeft te weten of een inlognaam of wachtwoord correct is.
Oké.
zal ik onthouden!
Even iets anders:
Mijn mysqli prepared statement voor het invoeren van de token gegevens doet het niet.
Dit is mijn code:
<?php
$sql = 'INSERT INTO 2fa (cookieID, token, expire-date) VALUES (?, ?, ?);';
$stmt = mysqli_stmt_init($conn);
if(!mysqli_stmt_prepare($stmt, $sql)) {
echo 'mysqli insert token statement is mislukt.';
exit();
}
else {
mysqli_stmt_bind_param($stmt, "sss", $cookieID, $hashedToken, $expires);
mysqli_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
?>

Ik krijg de hele tijd het bericht: mysqli insert token statement is mislukt.
Wat dus betekent dat er iets fout is in de $sql variabel.
Als ik probeer: echo error_get_last(), krijg ik geen error message te zien. Gewoon helemaal niets.
In de $sql zijn alle kolommen goed ingevoerd, zonder spelfouten.
@Jin:
SMS is beter, maar kost inderdaad geld (niet heel veel, kan al vanaf een paar cent), en is tegenwoordig ook vrij goed "af te vangen" (voor de echte doorzetter qua crimineel): https://www.howtogeek.com/310418/why-you-shouldnt-use-sms-for-two-factor-authentication/

@Frank:
Met een code "via de mail" heb je in ieder geval een tweede slot op dezelfde deur gedaan (naast de 1e autorisatie op je site - meestal een wachtwoord) (en misschien zit op de mail ook wel weer een 2FA - nu wel met SMS die iemand ander betaalt). Dat een gebruiker beide sleutels aan dezelfde ring hangt (of dat het zelfs dezelfde sleutel is!) kun je ook niets meer aan doen. Kortom: nog steeds beter dan niet doen.

@Jin:
Ik zou m'n code case insensitive maken. In principe volstaat enkel cijfers ook al (je hebt toch maar beperkt de tijd, en je kunt nog inbouwen dat je ook maar een beperkt aantal kansen heb - vergelijk de PIN-code op je bankpas: slechts 4 cijfers, maar ook maar 3 pogingen = toch veilig). Hoofd en kleine letters snapt niet iedereen, en sowieso moet je I (hoofdletter i), 1 (cijfer een), en l (kleine letter L) niet gebruiken (en zo zijn er nog wat, hoofdletter o, cijfer nul, enz).

@Algemeen:
Ik heb dit ooit maar platgeslagen door meerdere autorisatie methoden (MFA) te maken (en dus altijd uit te breiden als er weer een andere mogelijkheid bij komt). Voorbeelden hiervan zijn "password", "mail" (= code zoals hierboven), "token" (= token in cookie/storage), "subnet" (= juiste IP of reeks), "http" (= old scool HTTP autorisatie), enz. Elke methode geeft een true (alles OK), false (niet OK), of null (misschien, meer info nodig, bijvoorbeeld een wachtwoord, code, enz). Een "token" autorisatie kan dus in 1x true geven als het token al aanwezig is - hoeft de gebruiker niks meer voor te doen.

In de controller kan ik dan aangeven welke setjes van autorisatie methoden volstaan. Bovenstaande vertaalt zich dan naar:
- "password" en "mail", of:
- "password" en "token"
Als de gebruiker nog aan geen enkele set voldoet neem ik de set waarvoor ie het minste extra hoeft te doen (dus bij voorkeur degene met nog maar 1 ontbrekende autorisatie). Zijn er daar meer van, dan de 1e qua volgorde. In bovenstaand voorbeeld dus "password" (gewoon inloggen) en "mail" (code invoeren). Heeft ie dat gedaan dan plaats ik meteen een token, en hoeft ie voortaan (op dit apparaat) alleen nog maar een wachtwoord in te voeren (dan voldoet ie namelijk meteen aan de 2e set) (totdat het token verloopt).

Met die subnet methode kun je dan bijvoorbeeld iets doen ala
- "password" en "subnet", of:
- "password" en "mail", of:
- "password" en "token"
Zit je dus op het juiste subnet (bijvoorbeeld je lokale netwerk), dan hoef je de 2FA niet te doorlopen.

Andere autorisatie is "geo" die alleen true geeft als je uit Nederland komt (of hetzelfde land als de vorige keer). Zit je daar buiten dan moet je weer even (eenmalig) iets extra's doen (ja, ik weet van het bestaan van VPN's af).

Reageren