Session Login met Cookie onthouden
Ik heb een website gemaakt met een loginsysteem dmv $_SESSIONS maar nu klaagde sommige mensen dat ze iedere keer uitgelogd waren. Ik vond het zelf ook wel vervelend dus ik heb nu een $_COOKIE variant verzonnen maar omdat het mijn eerste keer is heb ik daar toch wel twijfels over.
Daarom hoor ik hier graag op of aanmerkingen. (In mijn $_SESSION sla ik de userid van het ingelogde account op waardoor je op iedere pagina bent aangemeld.)
Ten eerste heb ik in mijn database een tabel gemaakt met de kolommen:
- id (PRIMARY,AUTO_INCREMENT)
- userid
- cookiehash (UNIQUE)
- timestamp
Vervolgens heb ik dit in mijn inlogscript verwerkt:
Code (php)
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
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
<?php
// Als "Onthoud Mij" is aangeklikt
if(isset($_POST['onthoudmij'])) {
$cookiehash = md5(uniqid(microtime(), true));
$SQLSelectLoginSessies = "SELECT userid
FROM login_sessies
WHERE userid = '" . $row['id'] . "'";
$ResultLoginSessies = $conn->query($SQLSelectLoginSessies);
if ($ResultLoginSessies->num_rows > 0) {
$sqlUpdateLoginSessies = "UPDATE login_sessies
SET cookiehash='" . $cookiehash . "'
WHERE userid='" . $row['id'] . "'";
if ($conn->query($sqlUpdateLoginSessies) === TRUE) {
setcookie("Onthoudmij", $cookiehash, time() + (86400 * 30), "/", null, true, true); // 30 dagen
header('Location: ' . $terug . '');
exit;
} else {
header('Location: ' . $pagina . '&code=0003'); // Kan cookie niet zetten.
exit;
}
} else {
$SQLInsertLoginSessies = "INSERT INTO login_sessies (userid,cookiehash)
VALUES ('" . $row['id'] . "','" . $cookiehash . "')";
if ($conn->query($SQLInsertLoginSessies) === TRUE) {
setcookie("Onthoudmij", $cookiehash, time() + (86400 * 30), "/", null, true, true); // 30 dagen
header('Location: ' . $terug . '');
exit;
} else {
header('Location: ' . $pagina . '&code=0003'); // Kan cookie niet zetten.
exit;
}
}
}
?>
// Als "Onthoud Mij" is aangeklikt
if(isset($_POST['onthoudmij'])) {
$cookiehash = md5(uniqid(microtime(), true));
$SQLSelectLoginSessies = "SELECT userid
FROM login_sessies
WHERE userid = '" . $row['id'] . "'";
$ResultLoginSessies = $conn->query($SQLSelectLoginSessies);
if ($ResultLoginSessies->num_rows > 0) {
$sqlUpdateLoginSessies = "UPDATE login_sessies
SET cookiehash='" . $cookiehash . "'
WHERE userid='" . $row['id'] . "'";
if ($conn->query($sqlUpdateLoginSessies) === TRUE) {
setcookie("Onthoudmij", $cookiehash, time() + (86400 * 30), "/", null, true, true); // 30 dagen
header('Location: ' . $terug . '');
exit;
} else {
header('Location: ' . $pagina . '&code=0003'); // Kan cookie niet zetten.
exit;
}
} else {
$SQLInsertLoginSessies = "INSERT INTO login_sessies (userid,cookiehash)
VALUES ('" . $row['id'] . "','" . $cookiehash . "')";
if ($conn->query($SQLInsertLoginSessies) === TRUE) {
setcookie("Onthoudmij", $cookiehash, time() + (86400 * 30), "/", null, true, true); // 30 dagen
header('Location: ' . $terug . '');
exit;
} else {
header('Location: ' . $pagina . '&code=0003'); // Kan cookie niet zetten.
exit;
}
}
}
?>
En bovenaan mijn index.php pagina dit:
Code (php)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// Check voor Cookie
if(isset($_COOKIE['Onthoudmij'])) {
$CheckCookie = mysqli_real_escape_string($conn,$_COOKIE['Onthoudmij']);
$SQLCheckCookie = "SELECT userid,cookiehash
FROM login_sessies
WHERE cookiehash = '" . $CheckCookie . "'";
$ResultCheckCookie = $conn->query($SQLCheckCookie);
if ($ResultCheckCookie->num_rows > 0) {
while($RowLoginSessies = $ResultCheckCookie->fetch_assoc()) {
$_SESSION['user'] = $RowLoginSessies['userid'];
}
}
}
?>
// Check voor Cookie
if(isset($_COOKIE['Onthoudmij'])) {
$CheckCookie = mysqli_real_escape_string($conn,$_COOKIE['Onthoudmij']);
$SQLCheckCookie = "SELECT userid,cookiehash
FROM login_sessies
WHERE cookiehash = '" . $CheckCookie . "'";
$ResultCheckCookie = $conn->query($SQLCheckCookie);
if ($ResultCheckCookie->num_rows > 0) {
while($RowLoginSessies = $ResultCheckCookie->fetch_assoc()) {
$_SESSION['user'] = $RowLoginSessies['userid'];
}
}
}
?>
Dus wanneer er een cookie is gevonden op de clïent met een hash die overeenkomt met een hash in mijn tabel dan wordt dezelfde sessie gemaakt als wanneer je voorheen met sessies inlogde.
Doordat de kolom cookiehash in mijn tabel UNIQUE moet zijn kan je nooit een andere userid krijgen.
Gewijzigd op 19/01/2016 16:22:16 door Danny von Gaal
Alvast bedankt!
Grappig, want sessies gebruiken ook gewoon cookies. In plaats van te proberen een andere manier te verzinnen is het veel verstandiger om de reden van het uitloggen te achterhalen.
Ben van Velzen op 19/01/2016 16:18:38:
Grappig, want sessies gebruiken ook gewoon cookies. In plaats van te proberen een andere manier te verzinnen is het veel verstandiger om de reden van het uitloggen te achterhalen.
Hallo Ben,
Ik weet dat je de session lifetime kan verlengen maar dat leek mij zo onveilig??
Ook zou ik, als je zo'n doorstart maakt, de cookiehash verversen. EDIT: of misschien nog beter, ververs deze zolang je sessie bestaat.
Daarnaast zou je kunnen overwegen om een langere hash te pakken zodat deze moeilijker te raden is.
Zo te zien maak je al gebruik van HTTPS? Volgens mij is deze opzet dan redelijk veilig (dataverkeer kan niet ontcijferd worden en cookies kunnen alleen via HTTP ingesteld worden), but don't take my word for it.
Op dit moment is de hash het enige identificerende attribuut. Afhankelijk van de situatie zou je ook een IP of user agent kunnen verwerken in/bij de hash.
Gewijzigd op 19/01/2016 16:54:19 door Thomas van den Heuvel
Ik zal inderdaad boven de isset($_COOKIE eerst een check op mijn user session maken. En ik gebruik inderdaad HTTPS:// en via php.net zag ik dat je cookies op HTTPS kan afdwingen. Ik weet echter niet of dit nog nadelige gevolgen heeft.
En ik zat er ook aan te denken om de hash bij iedere opvraag van een pagina te verversen maar ik was bang dat de load naar de database dan te groot zal worden. Of kan die wel veel hebben?
Momenteel gebruik ik voor de hash maar welke encryptie methode stel jij voor? Het lijkt mij nu al niet dat iemand dit zal raden aangezien het op niets gebasseerd is (een naam bijvoorbeeld).
Danny von Gaal op 19/01/2016 20:57:56:
De functie uniqid() genereert een pseudo-aselecte ID op basis van de systeemklok. Genereer maar eens 100 ID's in een for-loop en je zult zien dat het eerste gedeelte van de string oploopt met de tijd.
Vandaar deze waarschuwing:
Warning This function does not create random nor unpredictable strings. This function must not be used for security purposes. Use a cryptographically secure random function/generator and cryptographically secure hash functions to create unpredictable secure IDs.
Aangezien uniqid() al een string gebaseerd op de systeemklok geeft, heeft het niet zoveel zin om de $prefix in de eerste parameter met microtime() op de systeemtijd te zetten. Je doet dan namelijk iets dat de functie zelf al doet:
Je kunt hier beter met bijvoorbeeld mt_rand() een aselect getal genereren:
Je gebruikt al true voor de tweede parameter $more_entropy van uniqid() om de ID minder voorspelbaar te maken. Je kunt daar zelf een flinke schep bovenop doen door een aselect getal toe te voegen:
Aangezien mt_rand() al is gebruikt voor de prefix, zou ik echter een andere functie dan mt_rand() gebruiken voor deze postfix, bijvoorbeeld:
Code (php)
Tot slot zit de hashfunctie md5() nog in de weg. Die kun je wel gebruiken voor interne hashes, maar beter niet meer voor beveiligingsdoeleinden. Op zijn minst gebruik je SHA-1:
Code (php)
Nog beter is een langere en sterkere variant uit de SHA-familie:
Ook mt_rand() is niet cryptographically secure.
In dat artikel wordt het concept nonce aangehaald, wat wellicht een idee is om te gebruiken, maar heeft misschien ook haken en ogen.
In dit artikel staat wel een aardig idee voor een IP-loze oplossing met net iets meer "identificatie" dan simpelweg een hash, en ook in de user comments staan goede aanvullingen, met name een veilig(er) alternatief voor mt_rand():
lezen van /dev/urandom (unix).
Op PHP.net worden ook enkele alternatieven genoemd: random_int(), random_bytes() of openssl_random_pseudo_bytes() (die hierboven al gebruikt wordt) en ook daar wordt in de comments /dev/urandom aangehaald.
Danny von Gaal op 19/01/2016 16:05:27:
Ten eerste heb ik in mijn database een tabel gemaakt met de kolommen:
- id (PRIMARY,AUTO_INCREMENT)
- userid
- cookiehash (UNIQUE)
- timestamp
[...]
Doordat de kolom cookiehash in mijn tabel UNIQUE moet zijn kan je nooit een andere userid krijgen.
- id (PRIMARY,AUTO_INCREMENT)
- userid
- cookiehash (UNIQUE)
- timestamp
[...]
Doordat de kolom cookiehash in mijn tabel UNIQUE moet zijn kan je nooit een andere userid krijgen.
En dat in combinatie met de query:
Code (php)
1
2
3
4
5
2
3
4
5
<?php
$SQLCheckCookie = "SELECT userid,cookiehash
FROM login_sessies
WHERE cookiehash = '" . $CheckCookie . "'";
?>
$SQLCheckCookie = "SELECT userid,cookiehash
FROM login_sessies
WHERE cookiehash = '" . $CheckCookie . "'";
?>
Dat is namelijk niet zomaar een random ID, maar een GUID of UUID. Ik begrijp wel waarom Danny uniqid() heeft gebruikt voor het genereren van die Global/Universal Unique ID, maar ik denk dat de functie UUID() van MySQL in een CHAR(36)-kolom daarvoor een beter alternatief is.
Nou is UUID() wel uniek, maar helaas ook voorspelbaar en daardoor te raden. Als je daar echter nog een (pseudo)random ID aan toevoegt — of naast zet, voor mijn part in een aparte kolom tot het maximum CHAR(255) — dan heb je een oplossing die zowel uniek als onvoorspelbaar is.
Ik vind de discussie zeker interessant, maar wil tegelijkertijd eigenlijk ook wel naar een soort van concrete aanbeveling toe (en Danny waarschijnlijk ook :D).
Code (php)
Maar om nog even terug te komen op dat de hash steeds wordt vernieuwd? Dan moet ik bij iedere pagina opvraag dus mijn database updaten en een nieuwe cookie setten? Zal dat uiteindelijk met veel gebruikers niet teveel verkeer generen en mijn database belasten? Of doen alle sites dat?
Ik zou het wel fijn vinden want dan kan ik laten zien wanneer de gebruiker voor het laatst online was. :)
Elke sessie heeft een eigen, unieke sessie-ID. De cookie-UUID maakt daarvan een geldige sessie — of meer precies: een sessie met een bekende user-ID.
Zou je de cookie-UUID nooit veranderen, dan krijg je iets dat lijkt op een eeuwigdurende sessie. Dat is het echter niet: het is een keten van opeenvolgende sessies met eigen sessie-ID's. Alles dat je daarbij in eerdere sessie hebt opgeslagen, gaat verloren. De nieuwe sessie bevat in deze opzet namelijk enkel en alleen de user-ID.
Er is dus vooral een directe één-op-één-relatie tussen de cookie-ID en de user-ID. Dat je het geheel beheert via sessies, is daarin maar bijzaak.
Voor de beveiliging betekent dit dat je dit cookie slechts beperkt moet vertrouwen: het geeft het vermoeden dat je te maken hebt met de user-ID van een terugkerende bezoeker. Niet meer, niet minder.
De rest van de beveiliging moet je daarop voortbouwen: wanneer is dat vermoeden voldoende en wanneer niet? Bijvoorbeeld voor het wijzigen van het wachtwoord, een e-mailadres en andere account- of gebruikersgegevens zul je dan opnieuw inloggen verplicht moeten stellen. Sommige webwinkels gebruiken vergelijkbare criteria: je kunt als terugkerende klant direct gaan winkelen, maar als je de order wilt bevestigen of wilt afrekenen, moet je even opnieuw inloggen.
Momenteel set ik een cookie die 30 dagen geldig is. Maar het is me al opgevallen dat iedere keer dat ik weer op de site kom de cookie niet vanzelf wordt bijgewerkt. Uiteindelijk zal je dus na 30 dagen weer afgemeld zijn zelfs als je er elke dag komt. Dat zou ik dus ook voorkomen als ik iedere keer een nieuwe cookie set.
Wanneer je namelijk op je pc en telefoon inlogt en je kiest bij beide voor onthouden dan veranderd de code in de db waardoor je op het andere apparaat automatisch bent uitgelogd. :(
Iemand ideeen?
Gewoon met een koppeltabel werken zodat je meerdere "sessies" aan 1 gebruiker kunt hangen? Dan kun je per entry ook een maximale levensduur hangen zodat je bijvoorbeeld na een maand of een jaar zonder gebruik van een bepaalde hash deze kan uitwissen.