Passwordless Login со CodeIgniter 4 & JWT – Комплетен Туторијал

Преглед на проектот

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/loginAuth::loginПрикажи login форма
POST/gen_login_urlAuth::genLoginUrlГенерирај JWT и испрати email
GET/auth/do_login?token=...Auth::doLoginВалидирај JWT и логирај
GET/profileAuth::profileПрофил страница (заштитена)
GET/logoutAuth::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"
  }
}
ПолеОпис
iatIssued At: кога е креиран токенот (Unix timestamp)
expExpiration: кога истекува (iat + 300 = 5 минути)
data.emailEmail адреса на корисникот
data.agentUser-Agent на прелистувачот (безбедносна проверка)
data.ip_addressIP адреса (безбедносна проверка)

Безбедносни механизми

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

Стани премиум член и доби пристап до сите содржини, специјален попуст на над 2.200 производи во ИТ маркет, верификуван профил и можност за огласување на ИТ Огласник. Плус ќе го поддржиш медиумот кој го градиме цели 16 години!

basic

членство

42 ден./мес

зачлени се

1337

членство

125 ден./мес

зачлени се
* плаќањето е на годишно ниво

Доколку веќе имаш премиум членство, најави се тука.

Добивај известувања
Извести ме за
guest
0 Коментари
Најнови
Најстари Со највеќе гласови
Inline Feedbacks
View all comments
види ги сите огласи на kariera.it.mk