0
Starting from the idea that the user entered the login and password correctly on the login page, I will save a cookie to authenticate the user so that the login remains active and the user can connect to other devices without being logged in in the previous session.
For this I started creating a table in the database with the following features:
CREATE TABLE `manter_login` (
`id` int(11) NOT NULL,
`id_user` int(11) NOT NULL,
`login_history` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`login_history`))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `manter_login`
ADD PRIMARY KEY (`id`),
ADD KEY `id_user` (`id_user`);
ALTER TABLE `manter_login`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
COMMIT;
And creating the following functions:
define("USER2", 'root');
define("PASS2", '');
define("NAME2", 'users');
//conexão area de login
try {
$db2 = new PDO("mysql:host=".HOST.";dbname=".NAME2.";charset=utf8", "".USER2."", "".PASS2."");
$db2->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db2->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
$db2->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
} catch (PDOException $e2) {
echo $e2->getMessage();
}
// Função basica para gerenciamento PDO
function sql2($db2, $q, $params, $return) {
// Prepare statement
$stmt = $db2->prepare($q);
// Execute statement
$stmt->execute($params);
// Decida se deseja retornar as linhas ou apenas contar as linhas
if ($return == "rows") {
return $stmt->fetch();
}elseif ($return == "rowsall") {
return $stmt->fetchAll();
}elseif ($return == "count") {
return $stmt->rowCount();
}elseif( $return == "lastid"){
return $db2->lastInsertId();
}elseif( $return == "fake"){
//fake sem nada
}
}
function salvarDadosLoginCookie($user_id, $removeSeletorLogin = 0){
global $db2;
$contar_registros = sql2($db2, "SELECT id FROM manter_login WHERE id_user = ?", array($user_id), "count");
$pega_ip = filter_input(INPUT_SERVER, 'HTTP_CF_CONNECTING_IP', FILTER_VALIDATE_IP);
$seletor = base64_encode(random_bytes(9));
$autenticador = random_bytes(33);
$meu_token = $seletor.':'.base64_encode($autenticador);
//Array com as informações do usuario para podermos autenticar mais tarde
$novoArray = [
'ip' => $pega_ip,
'seletor' => $seletor,
'autenticador' => hash('sha256', $autenticador),
'data' => date('Y-m-d h:i:s', time())
];
//Se a variavel removeSeletorLogin for diferente de zero, e porque a função foi chamada apos validar o hash_equals, então precisamos dar update no array e remover o seletor, para que não possa ser mais usado
if($removeSeletorLogin !== 0){
$lrdados = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($user_id), "rows");
$gerarNovoArrayRemovendoSeletor = removeArrayLogin($removeSeletorLogin, $lrdados['login_history']);
sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array($gerarNovoArrayRemovendoSeletor, $user_id), "fake");
}
//Se não houver registros iremos inserir um novo com o array de informações, Se ja exister um registro, o mesmo será modificado e iremos acrescentar um novo indice de array a ele, Esse Array poderá ter ate 5 indices, ou seja iremos permitir o uso da conta de um usuario a 5 dispostivos
if($contar_registros > 0){
$ldados = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($user_id), "rows");
$gerarNovoArray = updateArrayLogin($ldados['login_history'], $novoArray);
//Inciaremos esse session para poder fazer logout desse seletor mais tarde
$_SESSION['seletor_login_atual'] = $seletor;
sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array($gerarNovoArray, $user_id), "fake");
}else{
//Array Inicial
$novoArrayInsert = array($novoArray);
sql2($db2, "INSERT INTO manter_login(id_user, login_history) VALUES (?, ?)", array($user_id, json_encode($novoArrayInsert)), "fake");
}
// Cookie com token e id do usuario, esse id será usado para validar se existe um seletor para um usuario, se houver um seletor pra ele, iremos permitir o login, se não houver o login e recusado
setcookie('user_valid', json_encode(array( 'user_id' => $user_id, 'token' => $meu_token)), time() + (86400 * 7), '/', null, true, true);
}
function autenticarLogin(){
global $db2;
// Verificamos se o cookie existe
if (!isset($_COOKIE['user_valid']) || empty($_COOKIE['user_valid'])) {
return false;
}
// Verificamos se o cookie e valido
if(!$cookie_login = @json_decode($_COOKIE['user_valid'], true)) {
return false;
}
// Verificamos se os parametros estão corretos
if (!(isset($cookie_login['user_id']) || isset($cookie_login['token']))) {
return false;
}
// Verificamos o tamanho do cookie
if(strlen($_COOKIE['user_valid']) > 1500){
return false;
}
// Verificamos se o separador existe
if ((strpos($cookie_login['token'], ':') !== false) === false) {
return false;
}
$id_user_by_cookie = abs($cookie_login['user_id']);
// Verificamos se existe algum registro com o id do usuario no banco de dados
$contar_registros_user = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($id_user_by_cookie), "count");
if($contar_registros_user === 0){
return false;
}
if (empty($_SESSION['user_logado_id']) && !empty($_COOKIE['user_valid'])) {
list($seletor, $autenticador) = explode(':', $cookie_login['token']);
$registro_atual = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($id_user_by_cookie), "rows");
$encoda_login_history = json_decode($registro_atual['login_history'], true);
// definido inicialmente como 0, se no foreach houver um seletor igual ao cookie, iremos definir o valor pra 1
$verifica_existencia_seletor = 0;
$captura_autenticador = '';
$captura_ip = '';
$captura_seletor = '';
//Vamos descobrir se os seletores do usuario pesquisado e igual ao que está no cookie
foreach($encoda_login_history as $hlogin => $data) {
if($data['seletor'] == $seletor){
//Seletor Foi encontrado então iremos alterar o valor pra 1
$verifica_existencia_seletor = 1;
//Passamos o valor do token autenticador pra variavel
$captura_autenticador = $data['autenticador'];
//Passamos o ip para uma variavel externa para podermos validar tbm
$captura_ip = $data['ip'];
//Pegamos o seletor
$captura_seletor = $data['seletor'];
//vou limitar a vida de um cookie a 7 dias, ja que geramos um novo sempre que o session leva update
if(calculaDiferencaData($data['data']) >= 7){
//Removemos o Indice do array que tem o seletor gerado a mais de 7 dias
sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array(removeArrayLogin($seletor, $registro_atual['login_history']), $registro_atual['id_user']), "fake");
//Nesse caso o seletor deixou de existir, então voltaremos o valor dele para zero
$verifica_existencia_seletor = 0;
}
}
}
//Verificamos se o seletor foi encontrado
if($verifica_existencia_seletor === 0){
setcookie('user_valid', null);
return false;
}
$pega_ip = filter_input(INPUT_SERVER, 'HTTP_CF_CONNECTING_IP', FILTER_VALIDATE_IP);
//Se os ips do banco de dados e o ip da maquina do usuario forem diferentes não iniciaremos a sessao de login
if($captura_ip !== $pega_ip){
return false;
}
//Validamos o hash - se estiver tudo ok, podemos iniciar a session
if (hash_equals($captura_autenticador, hash('sha256', base64_decode($autenticador)))) {
//Inciaremos a session com id do usuario
$_SESSION['user_logado_id'] = $registro_atual['id_user'];
//Agora que o login foi bem sucessido, iremos re-criar o cookie e deletar o seletor do array, para que ele não possa ser utilizado novamente
salvarDadosLoginCookie($registro_atual['id_user'], $captura_seletor);
return true;
}
}
if(isset($_SESSION['user_logado_id'])){
return true;
}
}
//Função destinada para deslogar o dispositivo que o usuario ta logado
function deslogarDispositivoAtual(){
global $ta_logado, $db2;
if($ta_logado === 1){
$contar_registros_logout = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "count");
if($contar_registros_logout > 0){
$registros_logout = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "rows");
sql2($db2, "UPDATE manter_login SET login_history = ? WHERE id_user = ?", array(removeArrayLogin($_SESSION['seletor_login_atual'], $registros_logout['login_history']), $_SESSION['user_logado_id']), "fake");
}
}
session_destroy();
setcookie('user_valid', null);
}
//Função para usar quando o usuario mudar a senha
function deslogarDeTodosDispositivos($id_deslogar){
global $db2;
sql2($db2, "DELETE FROM manter_login WHERE id_user = ?", array($id_deslogar), "fake");
session_destroy();
setcookie('user_valid', null);
}
function calculaDiferencaData($DataEvento){
$hoje = new DateTime();
$diferenca = $hoje->diff(new DateTime($DataEvento));
return $diferenca->days;
}
function updateArrayLogin($arrayAtual, $novoArray){
$arrayAtual = json_decode($arrayAtual, true);
if(!is_array($arrayAtual)){
$arrayAtual = array();
}
$arrayAtual[] = $novoArray;
$arrayNovo = array_filter($arrayAtual, function($value) {
return is_array($value);
});
if(($quantidade = count($arrayNovo)) > 5){
$arrayNovo = array_slice($arrayNovo, $quantidade - 5, 5);
}
return json_encode($arrayNovo);
}
function removeArrayLogin($seletor, $arrayEdit){
$arrayEdit = json_decode($arrayEdit, true);
if(!is_array($arrayEdit)){
$arrayEdit = array();
}
foreach($arrayEdit as $arrayE => $data) {
if($data['seletor'] == $seletor){
unset($arrayEdit[$arrayE]);
}
}
return json_encode($arrayEdit);
}
and finally starting the functions to check if the user is logged in
$ta_logado = 0;
//Se o session user_logado_id não existe, iremos verificar se uma possivel sessão pode ser iniciada
if(!isset($_SESSION['user_logado_id']) || empty($_SESSION['user_logado_id'])){
autenticarLogin();
}
//Se ja existir user_logado_id, iremos chamar os dados no banco de dados
if(isset($_SESSION['user_logado_id']) && !empty($_SESSION['user_logado_id'])){
//Porem antes vamos validar e ver se o usuario não mudou a senha. Pois quando o usuario mudar a senha o registro no banco de dados e deletado.
$contar_registros_user_atual = sql2($db2, "SELECT * FROM manter_login WHERE id_user = ?", array($_SESSION['user_logado_id']), "count");
if($contar_registros_user_atual === 0){
session_destroy();
setcookie('user_valid', null);
}else{
$verificaDados = sql2($db2, "SELECT * from usuarios WHERE id = ?", array($_SESSION['user_logado_id']), "rows");
if(isset($verificaDados['usuario']) && !empty($verificaDados['usuario'])) {
$ta_logado = 1;
}
}
}
How can I improve the security of my system?
I switched to hmac and put a CHAVE_DA_APLICAO in the "$novoArray" array, because this way each session will have a CHAVE_DA_APLICAO itself
– Gabriel