Преглед на проектот
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







