PHP 2-Factor Autorisatie (2FA)

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Pagina: 1 2 3 4 5 6 volgende »

Jin vanTongeren

Jin vanTongeren

08/03/2019 18:29:40
Quote Anchor link
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,
 
PHP hulp

PHP hulp

21/11/2024 15:12:43
 
Nick Vledder

Nick Vledder

08/03/2019 20:21:59
Quote Anchor link
Up-to-date SSL-certificaat.
 
Jin vanTongeren

Jin vanTongeren

08/03/2019 20:23:02
Quote Anchor link
Ja oké, dat heb ik.
Maar nog iets anders?
(SSL = Let's Encrypt)
 
Rob Doemaarwat

Rob Doemaarwat

08/03/2019 20:40:12
Quote Anchor link
Je zou nog bij kunnen houden hoe vaak iemand al geprobeerd heeft om de code ($token) in te voeren. Na 3x "Helaas, U bent af. Genereer een nieuwe code en probeer het opnieuw.". En misschien nog een time-out tussen de pogingen (= tijdstip laatste poging opslaan).

Je zou alle data ook gewoon in de sessie op kunnen slaan. Dan is een kleine extra check dat ze in de aanvragende sessie zitten (en niet klaarzitten in een andere sessie om de 2FA code in te toetsen). Maar omdat je toch een vrij korte time-out hebt, en zal de sessie toch nog wel bestaan. Scheelt je weer gedoe in de database. Dit is alleen niet handig als je de code in een mailtje stuurt, met daarin een link om de code te valideren (misschien opent de link in een andere browser dan de gebruiker op dat moment zit - veel mensen hebben IE als "standaard browser", maar doen alles in Chrome).

Toevoeging op 08/03/2019 20:58:53:

O, en session_regenerate_id() na een succesvolle invoer.
 
- Ariën  -
Beheerder

- Ariën -

08/03/2019 21:07:52
Quote Anchor link
Zorg na een succesvolle login dat iemand browser fingerprint van zijn device opgeslagen wordt, zodat de 2FA niet continu wordt gevraagd. Waaronder een IP-adres.

En zorg voor uitgebreide mogelijkheden om inlogsessies te kunnen beheren, en dat je de gebruiker een andere sessie kan laten uitloggen.
Gewijzigd op 08/03/2019 21:08:07 door - Ariën -
 
Rob Doemaarwat

Rob Doemaarwat

08/03/2019 21:35:45
Quote Anchor link
IP blijft tricky: gebruikers die in beweging zijn gaan van Wifi naar 4G naar WiFi enz, en krijgen steeds een ander IP-adres.

Ook UserAgent string kan wijzigen, bijvoorbeeld als iemand tussen aanvragen 2FA code en invoeren een browser update door z'n strot kreeg (dan is het versienummer in de UA-string gewijzigd).
 
Thomas van den Heuvel

Thomas van den Heuvel

08/03/2019 22:09:22
Quote Anchor link
Mja maar wat is dan wel een oplossing? Kan niet het MAC-adres van iemand zijn netwerkkaart verkrijgen :p. Op den duur zul je aannames moeten doen.
 
Rob Doemaarwat

Rob Doemaarwat

08/03/2019 22:17:28
Quote Anchor link
Je zou gewoon een random code in cookie/localStorage/sessionStorage op kunnen slaan. Komt die code nog steeds overeen met wat je server-side ook hebt, dan is er geen reden om een nieuwe 2FA te doen.
 
Remco nvt

Remco nvt

09/03/2019 00:21:24
Quote Anchor link
Hoi,

Ik zou je adviseren TOTP (https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) zelf te implementeren.

Als je wat rond zoekt zijn er al veel github projecten te vinden met een implementatie daarvan. Als best practice wordt btw aangeraden op -1 ook goed te keuren, zodat als je code eigenlijk elke 30 seconden veranderd je ook die van tot 1 minuut geleden accepteert.

Om niet telkens iemand lastig te vallen zou ik een zogenaamde 'Trusted devices' lijst gaan bijhouden. Logt iemand in met zo'n device dan doe je geen 2FA voor de komende X dagen (vaak 30 dagen). Daarna moet de persoon weer op dat device ook een 2FA doen.
De lijst is iets van:
- user_id
- device_token
- last_2fa
En dan wat velden voor informatie die je wilt tonen aan de gebruiker. Vaak iets van:
- device_browser
- device_os
- device_initial_ip

De device_token sla je op in een cookie (secure en httpOnly).
Localstorage/sessionstorages etc. zou ik niet gebruiken. Niet alle browsers en mobile telefoons kunnen er mee overweg. Het is altijd JS en er zijn nog veel meer nadelen te benoemen.

Voordeel van zo'n lijst is dat je mensen die ook kan laten inzien en devices kan laten verdwijnen.
 
- Ariën  -
Beheerder

- Ariën -

09/03/2019 00:45:51
Quote Anchor link
Rob Doemaarwat op 08/03/2019 21:35:45:
IP blijft tricky: gebruikers die in beweging zijn gaan van Wifi naar 4G naar WiFi enz, en krijgen steeds een ander IP-adres.

Ook UserAgent string kan wijzigen, bijvoorbeeld als iemand tussen aanvragen 2FA code en invoeren een browser update door z'n strot kreeg (dan is het versienummer in de UA-string gewijzigd).

Daar heb je gelijk in, maar hoe zou facebook dat dan doen?
 
Rob Doemaarwat

Rob Doemaarwat

09/03/2019 09:01:47
Quote Anchor link
Geen idee. Wat ik wel weet is dat Twitter (die ik standaard in een "privacy" scherm open - ivm alle links naar externe sites) elke keer als ik de browser start 2FA doet/vraagt. Die slaat dus geen "device fingerprint" op maar gewoon iets van een cookie/storage (die dus steeds verdwijnt als ik de browser/computer herstart).
 
Jin vanTongeren

Jin vanTongeren

09/03/2019 09:50:31
Quote Anchor link
Zou dit genoeg zijn voor het genereren van een code en qua veiligheid?
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?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();                
                }                
            }
        }
    }
}
 
Frank Nietbelangrijk

Frank Nietbelangrijk

09/03/2019 10:05:42
Quote Anchor link
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.

Toevoeging op 09/03/2019 10:07:30:

@Jin: $_GET variabelen zijn nooit zeker. check voor gebruik of ze bestaan met isset().
 
Jin vanTongeren

Jin vanTongeren

09/03/2019 12:16:44
Quote Anchor link
Maar een code via de sms kost toch geld / beltegoed? Of ben ik nou mis?
 
Thomas van den Heuvel

Thomas van den Heuvel

09/03/2019 12:45:26
Quote Anchor link
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".
Gewijzigd op 09/03/2019 12:46:04 door Thomas van den Heuvel
 
Jin vanTongeren

Jin vanTongeren

09/03/2019 12:49:33
Quote Anchor link
@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)
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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>';
                    }    
                }
?>
 
Thomas van den Heuvel

Thomas van den Heuvel

09/03/2019 12:53:20
Quote Anchor link
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.
 
Jin vanTongeren

Jin vanTongeren

09/03/2019 12:55:56
Quote Anchor link
Dus ik moet de gebruikers gewoon terugsturen zonder enige informatie voor de gebruikers of ze het goed / niet goed hebben gedaan?
 
- Ariën  -
Beheerder

- Ariën -

09/03/2019 12:59:46
Quote Anchor link
Gewoon enkel bij inlogfouten melden dat "het inloggen niet gelukt is". Niemand hoeft te weten of een inlognaam of wachtwoord correct is.
Gewijzigd op 09/03/2019 13:00:38 door - Ariën -
 
Jin vanTongeren

Jin vanTongeren

09/03/2019 13:05:02
Quote Anchor link
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:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
<?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.
 
Rob Doemaarwat

Rob Doemaarwat

09/03/2019 13:16:38
Quote Anchor link
@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).
Gewijzigd op 09/03/2019 13:18:39 door Rob Doemaarwat
 

Pagina: 1 2 3 4 5 6 volgende »



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.