Saturday, July 13, 2024
CodeIgniter - authentication
Before this, take a look at this
post.
The code is on
github.
The whole process goes like this:
- user registration
- the user sends an email and password
- if the email does not exist, create a new user account
- user login
- the user sends an email and password
- if the email exists - register the user
When registering, we enter the email, code and user id (UUID) in the database.
When the user logs in, we send him a (http-only) browser-only cookie. It means a cookie that cannot be reached with javascript in the internet browser. In the session, let's add the key ["user-logged-on" => 'f750204d-e73d-43e7-a5c2-cff967d233c6'].
On the next visit, we check (via cookie) "user-logged-on" from the session.
On logout, we delete the session key.
In phpMyAdmin (or through the terminal) we create a table:
DROP DATABASE IF EXISTS ci_login_app;
CREATE DATABASE ci_login_app;
DROP USER IF EXISTS 'ci_login_app'@'localhost';
CREATE USER 'ci_login_app'@'localhost' IDENTIFIED BY 'ci_login_app';
GRANT ALL ON ci_login_app.* TO 'ci_login_app'@'localhost';
USE ci_login_app;
CREATE TABLE users (
id int(11) NOT NULL AUTO_INCREMENT,
email varchar(320) DEFAULT NULL,
password varchar(256) DEFAULT NULL,
uuid varchar(36) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
We install CI4 and install libs:
composer create-project codeigniter4/appstarter ci_login_app
cd ./ci_login_app
composer require ramsey/uuid
composer require respect/validation
We copy env file to .env
cp ./env ./.env
...and change under "Database" and "Environment":
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT = development
#...
#--------------------------------------------------------------------
# DATABASE
#--------------------------------------------------------------------
database.default.hostname = localhost
database.default.database = ci_login_app
database.default.username = ci_login_app
database.default.password = ci_login_app
database.default.DBDriver = MySQLi
# database.default.DBPrefix =
We open an additional tab in the terminal and start the dev server:
php spark serve
The server is at:
http://localhost:8080
We create the "Users" controller and model:
php spark make:model Users --suffix
php spark make:controller Users --suffix
Then we create view templates:
touch ./app/Views/dashboard.php
touch ./app/Views/login.php
touch ./app/Views/logout.php
touch ./app/Views/register.php
We enter the routes in "./app/Config/Routes.php":
//ispod: $routes->get('/', 'Home::index');
$routes->get('/login', 'UsersController::login');
$routes->post('/login-post', 'UsersController::loginPost');
$routes->get('/register', 'UsersController::register');
$routes->post('/register-post', 'UsersController::registerPost');
$routes->get('/dashboard', 'UsersController::dashboard');
$routes->get('/logout', 'UsersController::logout');
We make changes to our “./app/Models/UserModel.php”:
<?php
namespace App\Models;
use CodeIgniter\Model;
class UsersModel extends Model
{
protected $DBGroup = 'default';
protected $table = 'users';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $insertID = 0;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = ['password','email','uuid'];
// Dates
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [
"password" => "required|min_length[8]|max_length[256]",
"email" => "required|valid_email|min_length[5]|is_unique[users.email]",
];
protected $validationMessages = [
"password" => [
"required" => "Password is required",
"min_length" => "Minimum length of password should be 8 chars",
"max_length" => "Maximum length of password should be 256 chars",
],
"email" => [
"required" => "Email needed",
"valid_email" => "Please provide a valid email address"
],
];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}
In the templates we enter: "./app/Views/login.php"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Login</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>
<body>
<div class="w3-panel w3-blue-grey w3-animate-top info-text-conatiner" style="margin:0; display:none;">
<h3>Information!</h3>
<p class="info-text"></p>
</div>
<div class="w3-card-4">
<header class="w3-container w3-blue">
<h1>Login</h1>
</header>
<div class="w3-container">
<br>
<form class="w3-container">
<input class="w3-input email-input" type="text">
<label>Email</label>
<input class="w3-input password-input" type="text">
<label>Password</label>
</form>
<br>
<button class="w3-button w3-blue" onclick="login()"> Submit </button>
<br>
<br>
</div>
</div>
</body>
</html>
<script>
const login = async () => {
const email = document.querySelector(".email-input");
const password = document.querySelector(".password-input");
const messageContainer = document.querySelector(".info-text-conatiner");
const messageContainerText = document.querySelector(".info-text");
if (email.value < 4)
{
console.log( "bad email" );
return null;
}
if (password.value < 4)
{
console.log( "bad password" );
return null;
}
const postData = new FormData();
postData.append("login-form-email", email.value);
postData.append("login-form-password", password.value);
try {
const response = await fetch('/login-post', {
method: 'POST',
body: postData,
});
const jsonUserData = await response.json();
messageContainer.style.display = 'block';
messageContainerText.innerHTML = jsonUserData.message;
if (!jsonUserData.error)
{
window.location = '/dashboard';
}
//console.log( jsonUserData.name );
}
catch (error) {
console.error(error);
}
}
</script>
.app/Views/logout.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Logged off!</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>
<body>
<div class="w3-card-4">
<header class="w3-container w3-blue">
<h1>Logged off!</h1>
</header>
<div class="w3-margin w3-padding">
<p>User logged off.</p>
<div class="w3-button w3-blue" onclick="window.location='/login'"> Go to login page. </div>
<br><br>
</div>
</div>
</body>
</html>
./app/Views/register.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Register</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>
<body>
<div class="w3-panel w3-blue-grey w3-animate-top info-text-conatiner" style="margin:0; display:none;">
<h3>Information!</h3>
<p class="info-text"></p>
</div>
<div class="w3-card-4">
<header class="w3-container w3-blue">
<h1>Register</h1>
</header>
<div class="w3-container">
<br>
<form class="w3-container">
<input class="w3-input email-input" type="text">
<label>Email</label>
<input class="w3-input password-input" type="text">
<label>Password</label>
</form>
<br>
<button class="w3-button w3-blue" onclick="register()"> Submit </button>
<br>
<br>
</div>
</div>
</body>
</html>
<script>
const register = async () => {
const email = document.querySelector(".email-input");
const password = document.querySelector(".password-input");
const messageContainer = document.querySelector(".info-text-conatiner");
const messageContainerText = document.querySelector(".info-text");
if (email.value < 4)
{
console.log( "bad email" );
return null;
}
if (password.value < 4)
{
console.log( "bad password" );
return null;
}
const postData = new FormData();
postData.append("register-form-email", email.value);
postData.append("register-form-password", password.value);
try {
const response = await fetch('/register-post', {
method: 'POST',
body: postData,
});
const jsonUserData = await response.json();
messageContainer.style.display = 'block';
messageContainerText.innerHTML = jsonUserData.message;
console.log( jsonUserData.name );
}
catch (error) {
console.error(error);
}
}
</script>
./app/Views/dashboard.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Dashboard</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>
<body>
<div class="w3-card-4">
<header class="w3-container w3-blue">
<h1>Dashboard</h1>
</header>
<div class="w3-margin w3-padding">
<p>Only logged user can see this.</p>
<div class="w3-button w3-blue" onclick="window.location='/logout'"> Logout </div>
<br><br>
</div>
</div>
</body>
</html>
And finally, we enter in “./app/Controllers/UsersController.php”:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\UsersModel;
use Ramsey\Uuid\Uuid;
use Respect\Validation\Validator as v;
class UsersController extends BaseController
{
public function index()
{
//
}
public function login()
{
$session = session();
if ($session->has('loginId'))
{
return redirect()->to('/dashboard');
}
return view("login");
}
public function loginPost()
{
header('Content-type:application/json;charset=utf-8');
$session = session();
$email = $this->request->getVar("login-form-email");
$password = $this->request->getVar("login-form-password");
if (!v::email()->validate($email))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad email format!',
'data' => null,
]);
die();
}
if (!v::alnum()->noWhitespace()->length(8, 40)->validate($password))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Password must be alphanumeric 8-40 chars!',
'data' => null,
]);
die();
}
$usersModel = new UsersModel();
$emailExists = $usersModel->getWhere(["email" => $email]);
$emailExistsResult = $emailExists->getResult();
if (!$emailExistsResult)
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'User email not found!',
'data' => null,
]);
die();
}
$passHash = $emailExistsResult[0]->password;
$uuid = $emailExistsResult[0]->uuid;
if (!password_verify($password, $passHash))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad login password!',
'data' => null,
]);
die();
}
else
{
$session->set('loginId', $uuid);
echo json_encode([
'error' => false,
'message' => 'User login valid!',
'data' => $uuid,
]);
die();
}
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Fail, internal server error.',
'data' => null,
]);
die();
}
public function register()
{
$session = session();
if ($session->has('loginId'))
{
return redirect()->to('/dashboard');
}
return view("register");
}
public function registerPost()
{
header('Content-type:application/json;charset=utf-8');
$session = session();
$email = $this->request->getVar("register-form-email");
$password = $this->request->getVar("register-form-password");
$uuid = Uuid::uuid4();
if (!v::email()->validate($email))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad email format!',
'data' => null,
]);
die();
}
if (!v::alnum()->noWhitespace()->length(8, 40)->validate($password))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Password must be alphanumeric 8-40 chars!',
'data' => null,
]);
die();
}
$passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost'=>12]);
$usersModel = new UsersModel();
$emailExists = $usersModel->getWhere(["email" => $email]);
$emailExistsResult = $emailExists->getResult();
if ($emailExistsResult)
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Email already exists!',
'data' => null,
]);
die();
}
$insertUserId = $usersModel->insert([
'email' => $email,
'password' => $passwordHash,
'uuid' => $uuid,
]);
if ($insertUserId > 0)
{
echo json_encode([
'error' => false,
'message' => 'User added.',
'data' => $uuid,
]);
die();
}
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Fail, internal server error.',
'data' => null,
]);
die();
}
public function dashboard()
{
return view("dashboard");
}
public function logout()
{
$session = session();
$session->remove('loginId');
return view('logout');
}
}
And finally, we enter in “./app/Controllers/UsersController.php”:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\UsersModel;
use Ramsey\Uuid\Uuid;
use Respect\Validation\Validator as v;
class UsersController extends BaseController
{
public function index()
{
//
}
public function login()
{
$session = session();
if ($session->has('loginId'))
{
return redirect()->to('/dashboard');
}
return view("login");
}
public function loginPost()
{
header('Content-type:application/json;charset=utf-8');
$session = session();
$email = $this->request->getVar("login-form-email");
$password = $this->request->getVar("login-form-password");
if (!v::email()->validate($email))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad email format!',
'data' => null,
]);
die();
}
if (!v::alnum()->noWhitespace()->length(8, 40)->validate($password))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Password must be alphanumeric 8-40 chars!',
'data' => null,
]);
die();
}
$usersModel = new UsersModel();
$emailExists = $usersModel->getWhere(["email" => $email]);
$emailExistsResult = $emailExists->getResult();
if (!$emailExistsResult)
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'User email not found!',
'data' => null,
]);
die();
}
$passHash = $emailExistsResult[0]->password;
$uuid = $emailExistsResult[0]->uuid;
if (!password_verify($password, $passHash))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad login password!',
'data' => null,
]);
die();
}
else
{
$session->set('loginId', $uuid);
echo json_encode([
'error' => false,
'message' => 'User login valid!',
'data' => $uuid,
]);
die();
}
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Fail, internal server error.',
'data' => null,
]);
die();
}
public function register()
{
$session = session();
if ($session->has('loginId'))
{
return redirect()->to('/dashboard');
}
return view("register");
}
public function registerPost()
{
header('Content-type:application/json;charset=utf-8');
$session = session();
$email = $this->request->getVar("register-form-email");
$password = $this->request->getVar("register-form-password");
$uuid = Uuid::uuid4();
if (!v::email()->validate($email))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Bad email format!',
'data' => null,
]);
die();
}
if (!v::alnum()->noWhitespace()->length(8, 40)->validate($password))
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Password must be alphanumeric 8-40 chars!',
'data' => null,
]);
die();
}
$passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost'=>12]);
$usersModel = new UsersModel();
$emailExists = $usersModel->getWhere(["email" => $email]);
$emailExistsResult = $emailExists->getResult();
if ($emailExistsResult)
{
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Email already exists!',
'data' => null,
]);
die();
}
$insertUserId = $usersModel->insert([
'email' => $email,
'password' => $passwordHash,
'uuid' => $uuid,
]);
if ($insertUserId > 0)
{
echo json_encode([
'error' => false,
'message' => 'User added.',
'data' => $uuid,
]);
die();
}
http_response_code(500);
echo json_encode([
'error' => true,
'message' => 'Fail, internal server error.',
'data' => null,
]);
die();
}
public function dashboard()
{
return view("dashboard");
}
public function logout()
{
$session = session();
$session->remove('loginId');
return view('logout');
}
}
Testing
I intentionally left the password field visible. When we go to the "/register" path, we get the registration form:
And we see the entry in the database:
At "/login" we of course have a login form:
And when we are connected, we can go to "/dashboard".
And finally, to log out the user, we go to the "/logout" route.