Преглед на проектот
Passwordless (без лозинка) автентикација систем користејќи JWT (JSON Web Tokens) и email magic links. Без база на податоци. Целата автентикација се базира на JWT токен + сесија.
Како работи?
1. Корисникот го внесува својот email во формата
2. Системот генерира JWT (со email, user-agent, IP адреса, валиден 5 минути
3. JWT се испраќа како линк на email на корисникот
4. Корисникот го отвора линкот -> системот го верификува JWT
5. Дополнително се проверува user-agent и IP адреса за безбедност
6. Ако сè е валидно -> сесија се креира -> корисникот е на профил страницата
Tech Stack
- Framework: CodeIgniter 4
- JWT Library: firebase/php-jwt
- Email: SMTP (Gmail)
- Frontend: Bootstrap 5
- PHP: 8.2+
- Database: Нема (не е потребна!)
Чекор 1: Инсталирај CodeIgniter
composer create-project codeigniter4/appstarter JWT-Login-Codeigniter-4
cd JWT-Login-Codeigniter-4
Чекор 2: Инсталирај Firebase PHP-JWT
composer require firebase/php-jwt
Чекор 3: Конфигурирај .env
Преименувај го env → .env и подеси го:
.env
#--------------------------------------------------------------------
# JWT
#--------------------------------------------------------------------
JWT_SECRET = "98asda7fa873kj1l3k4jasidufa"
#--------------------------------------------------------------------
# EMAIL CONFIG
#--------------------------------------------------------------------
email.fromEmail = ""
email.fromName = ""
email.recipients = ""
email.protocol = "smtp"
email.SMTPHost = "smtp.gmail.com"
email.SMTPUser = ""
email.SMTPPass = ""
email.SMTPPort = 587
email.SMTPCrypto = "tls"
email.mailType = "html"
#--------------------------------------------------------------------
# ENVIRONMENT
#--------------------------------------------------------------------
CI_ENVIRONMENT = development
#--------------------------------------------------------------------
# APP
#--------------------------------------------------------------------
app.baseURL = 'http://localhost:8000'
Gmail корисници: Не ја користите вашата обична лозинка! Генерирајте App Password од https://myaccount.google.com/apppasswords (потребна е 2-Step Verification).
Чекор 4: Email конфигурација
Креирајте го фајлот app/Config/Email.php. Овој фајл ги чита сите email поставки од .env фајлот.
app/Config/Email.php
<?php
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Email extends BaseConfig
{
public string $fromEmail;
public string $fromName;
public string $recipients;
public string $userAgent;
public string $protocol;
public string $mailPath;
public string $SMTPHost;
public string $SMTPUser;
public string $SMTPPass;
public int $SMTPPort;
public int $SMTPTimeout;
public bool $SMTPKeepAlive;
public string $SMTPCrypto;
public bool $wordWrap;
public int $wrapChars;
public string $mailType;
public string $charset;
public bool $validate;
public int $priority;
public string $CRLF;
public string $newline;
public bool $BCCBatchMode;
public int $BCCBatchSize;
public bool $DSN;
public function __construct()
{
parent::__construct();
$this->fromEmail = env('email.fromEmail', '[email protected]');
$this->fromName = env('email.fromName', 'My App');
$this->recipients = env('email.recipients', '');
$this->userAgent = env('email.userAgent', 'CodeIgniter');
$this->protocol = env('email.protocol', 'smtp');
$this->mailPath = env('email.mailPath', '/usr/sbin/sendmail');
$this->SMTPHost = env('email.SMTPHost', 'smtp.example.com');
$this->SMTPUser = env('email.SMTPUser', '');
$this->SMTPPass = env('email.SMTPPass', '');
$this->SMTPPort = (int) env('email.SMTPPort', 587);
$this->SMTPTimeout = (int) env('email.SMTPTimeout', 5);
$this->SMTPKeepAlive = (bool) env('email.SMTPKeepAlive', false);
$this->SMTPCrypto = env('email.SMTPCrypto', 'tls');
$this->wordWrap = (bool) env('email.wordWrap', true);
$this->wrapChars = (int) env('email.wrapChars', 76);
$this->mailType = env('email.mailType', 'html');
$this->charset = env('email.charset', 'UTF-8');
$this->validate = (bool) env('email.validate', false);
$this->priority = (int) env('email.priority', 3);
$this->CRLF = env('email.CRLF', "\r\n");
$this->newline = env('email.newline', "\r\n");
$this->BCCBatchMode = (bool) env('email.BCCBatchMode', false);
$this->BCCBatchSize = (int) env('email.BCCBatchSize', 200);
$this->DSN = (bool) env('email.DSN', false);
}
}
Зошто овој фајл? Стандардниот CI4 Email.php има hardcoded вредности. Овој го менува за сите поставки да се читаат од .env, што е побезбедно и полесно за конфигурација.
Чекор 5: Рути (Routes)
Додадете ги auth рутите во постоечкиот Routes файл.
app/Config/Routes.php
<?php
use CodeIgniter\Router\RouteCollection;
/**
* @var RouteCollection $routes
*/
$routes->get('/', 'Home::index');
$routes->get("login", "Auth::login");
$routes->post("gen_login_url", "Auth::genLoginUrl");
$routes->get("auth/do_login", "Auth::doLogin");
$routes->get("logout", "Auth::logout");
$routes->get("profile", "Auth::profile");
Преглед на рутите:
| Метод | URL | Контролер метод | Опис |
|---|---|---|---|
| GET | /login | Auth::login | Прикажи login форма |
| POST | /gen_login_url | Auth::genLoginUrl | Генерирај JWT и испрати email |
| GET | /auth/do_login?token=... | Auth::doLogin | Валидирај JWT и логирај |
| GET | /profile | Auth::profile | Профил страница (заштитена) |
| GET | /logout | Auth::logout | Одлогирај се |
Чекор 6: Auth Контролер (Главната логика)
Ова е срцето на апликацијата, JWT генерирање, валидација, и сесија менаџмент.
app/Controllers/Auth.php
<?php
namespace App\Controllers;
use Config\Services;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class Auth extends BaseController
{
/**
* Прикажи login форма
* Ако корисникот е веќе логиран → redirect на профил
*/
public function login()
{
if (!empty(session()->get("login"))) {
return redirect()->to(base_url('profile'))->with("success", "you already login");
}
return view('auth/login');
}
/**
* Верификувај го JWT токенот од magic link и логирај го корисникот
*
* Безбедносни проверки:
* 1. Дали токенот постои
* 2. Дали JWT е валиден и не е истечен (firebase/php-jwt го прави ова)
* 3. Дали User-Agent се совпаѓа (ист прелистувач)
* 4. Дали IP адресата се совпаѓа (иста мрежа)
*/
public function doLogin()
{
try {
$token = $this->request->getGet("token") ?? "";
if (empty($token)) {
echo "Token Not Found.";
exit;
}
// Декодирај го JWT токенот
$dJwt = (array) JWT::decode($token, new Key(env("JWT_SECRET"), "HS256"));
$data = (array) $dJwt["data"];
$email = $data["email"];
$agent = $data["agent"];
$ipAddress = $data["ip_address"];
// Безбедносна проверка: User-Agent мора да се совпаѓа
if ($agent != $this->request->getUserAgent()->getAgentString()) {
return redirect()->to(base_url('login'))->with("error", "Invalid URL");
}
// Безбедносна проверка: IP адресата мора да се совпаѓа
if ($ipAddress != $this->request->getIPAddress()) {
return redirect()->to(base_url("login"))->with("error", "Invalid URL");
}
// Сè е валидно — креирај сесија
session()->set("login", $email);
return redirect()->to(base_url('profile'))->with("success", "Login Success.");
} catch (\Throwable $th) {
return redirect()->to(base_url('login'))->with("error", "Error:" . $th->getMessage());
}
}
/**
* Генерирај JWT токен и испрати magic link на email
*
* JWT payload содржи:
* - iat: време на издавање
* - exp: време на истекување (iat + 300 секунди = 5 минути)
* - data.email: email адреса на корисникот
* - data.agent: User-Agent стринг на прелистувачот
* - data.ip_address: IP адреса на корисникот
*/
public function genLoginUrl()
{
try {
$email = $this->request->getPost("email");
// Валидација на email
$validation = Services::validation();
$validation->setRules([
'email' => [
'label' => 'Email',
'rules' => 'required|valid_email',
'errors' => [
'required' => 'Email is required.',
'valid_email' => 'Invalid email format.',
]
],
]);
if (!$validation->run(['email' => $email])) {
return redirect()->back()->withInput()->with('error', $validation->getError('email'));
}
// Подготви JWT payload
$key = getenv('JWT_SECRET') ?: 'your-secret-key';
$issuedAt = time();
$expire = $issuedAt + 300; // 5 минути
$agent = $this->request->getUserAgent()->getAgentString();
$ipAddress = $this->request->getIPAddress();
$payload = [
'iat' => $issuedAt,
'exp' => $expire,
'data' => [
"email" => $email,
"agent" => $agent,
"ip_address" => $ipAddress,
],
];
// Кодирај го JWT токенот
$token = JWT::encode($payload, $key, 'HS256');
// Генерирај го magic link URL
$loginUrl = base_url('auth/do_login?token=' . $token);
// Испрати email
$ciEmail = service('email');
$ciEmail->setTo($email);
$ciEmail->setSubject("Login URL");
$body = <<<EMAIL
Please Login with this url: {$loginUrl}
EMAIL;
$ciEmail->setMessage($body);
if ($ciEmail->send()) {
return redirect()->back()->with("success", "Login Url has been sent to your email.");
} else {
$debug = $ciEmail->printDebugger(['headers', 'subject', 'body']);
log_message('error', 'Send email failed: ' . json_encode($debug));
return redirect()->back()->with('error', 'Failed to create Login URL.');
}
} catch (\Throwable $th) {
return redirect()->to(base_url('login'))->with('error', 'Error: ' . $th->getMessage());
}
}
/**
* Профил страница — заштитена со сесија проверка
*/
public function profile()
{
if (!empty(session()->get("login"))) {
$vData = [
"email" => session()->get("login"),
];
return view("auth/profile", $vData);
}
return redirect()->to(base_url('login'))->with("error", "Please Login");
}
/**
* Одлогирај се — уништи ја сесијата
*/
public function logout()
{
session()->set("login", "");
session()->destroy();
return redirect()->to(base_url('login'));
}
}
Објаснување на клучните методи:
genLoginUrl(): Го генерира JWT токенот:
- Валидира email -> креира JWT payload (email + user-agent + IP)-> го кодира со HS256 -> формира URL -> испраќа email.
doLogin(): Го верификува magic link:
- Го декодира JWT -> проверува дали user-agent и IP се исти -> ако да, креира сесија.
profile(): Заштитена страница:
- Проверува
session()->get("login")-> ако нема сесија, redirect на login.
Чекор 7: Views (Прегледи)
app/Views/auth/login.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center justify-content-center vh-100">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h4 class="text-center mb-4">Login</h4>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= session('error') ?>
<button type="button" class="btn-close"
data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= session('success') ?>
<button type="button" class="btn-close"
data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<form method="post" action="<?= base_url('gen_login_url'); ?>">
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" name="email"
id="email" placeholder="[email protected]" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js">
</script>
</body>
</html>
Како работи:
- Формата праќа POST на
/gen_login_urlсоemailполето - Flash пораките (
error/success) се прикажуваат како Bootstrap alerts - Bootstrap 5 CDN за стилизирање
app/Views/auth/profile.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>User Profile</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom shadow-sm">
<div class="container">
<a class="navbar-brand fw-bold" href="#">MyApp</a>
<div class="ms-auto">
<a href="<?= base_url('logout'); ?>"
class="btn btn-outline-danger btn-sm">Logout</a>
</div>
</div>
</nav>
<div class="container d-flex flex-column align-items-center justify-content-center py-5">
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= session('error') ?>
<button type="button" class="btn-close"
data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= session('success') ?>
<button type="button" class="btn-close"
data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="card text-center shadow-sm" style="max-width: 400px; width: 100%;">
<div class="card-body">
<h5 class="card-title mb-0">Welcome,</h5>
<p class="text-muted mb-3"><?= $email ?? ""; ?></p>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js">
</script>
</body>
</html>
Како работи:
- Прикажува „Welcome,” + email адресата од
$email(пратена од контролерот) - Navbar со Logout копче -> линк на
/logout - Flash пораки за success/error
Чекор 8: Стартувај го проектот
php spark serve --port 8000
Отвори http://localhost:8000/login во прелистувачот.
JWT Payload — Што содржи токенот
{
"iat": 1738000000,
"exp": 1738000300,
"data": {
"email": "[email protected]",
"agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
"ip_address": "192.168.1.100"
}
}
| Поле | Опис |
|---|---|
iat | Issued At: кога е креиран токенот (Unix timestamp) |
exp | Expiration: кога истекува (iat + 300 = 5 минути) |
data.email | Email адреса на корисникот |
data.agent | User-Agent на прелистувачот (безбедносна проверка) |
data.ip_address | IP адреса (безбедносна проверка) |
Безбедносни механизми
1. Кратко времетраење (5 мин): $expire = $issuedAt + 300;
Токенот автоматски истекува после 5 минути. firebase/php-jwt фрла ExpiredException ако е истечен.
2. User-Agent проверка: Линкот мора да се отвори од истиот прелистувач каде е побарано логирање. Ако некој го украде линкот и го отвори од друг прелистувач нема да работи.
3. IP адреса проверка: Линкот мора да се отвори од истата IP адреса. Ова спречува употреба на линкот од друга мрежа/локација.
4. JWT потпис (HS256): Токенот е потпишан со JWT_SECRET. Ако некој го менува содржината на токенот, потписот нема да се совпаѓа и JWT::decode() ќе фрли грешка.
5. Try-catch error handling: Секоја грешка при декодирање (истечен, невалиден, менуван токен) се фаќа и корисникот се враќа на login со порака за грешка.
Обврнете внимание
- JWT_SECRET мора да биде уникатен и тајен стринг. Сменете го
"98asda7fa873kj1l3k4jasidufa"со вашуникатен - Gmail App Password – за Gmail SMTP потребна е 2FA + App Password (не обичната лозинка)
- Нема база – овој проект не користи база на податоци, автентикацијата е целосно преку JWT + сесии
- HTTPS е препорачано во продукција (линковите содржат JWT токен во URL)
email.fromEmailиemail.SMTPUserмора да бидат пополнети во.envза email да работи
Изворниот код можете да го најдете на github
Дискусија
Уште никој не искоментирал на темава, биди прв...