﻿# Panduan integrasi SSO Portal Sentral Medika

Dokumen ini dapat diberikan langsung kepada AI atau developer ketika membuat aplikasi baru yang menerima login dari Portal. Acuan implementasi adalah CodeIgniter 4, PHP, dan JWT HS256. Ini merupakan SSO khusus melalui redirect JWT, bukan implementasi OAuth2/OpenID Connect.

## 1. Instruksi untuk AI pembuat aplikasi baru

Implementasikan penerima SSO sesuai kontrak dokumen ini. Baca struktur project, versi framework, dependency, route, session, dan filter autentikasi sebelum mengubah kode. Pertahankan fitur yang sudah ada. Jangan menganggap cookie atau session Portal otomatis tersedia pada aplikasi penerima.

Isi parameter berikut sebelum implementasi:

| Parameter | Contoh Reminder | Contoh aplikasi baru |
| --- | --- | --- |
| URL Portal | https://portal.sentralmedika.co.id | sama |
| URL penerima | https://reminder.sentralmedika.co.id | https://inventaris.sentralmedika.co.id |
| Audience JWT | reminder-app | inventaris-app |
| Route pembuka di Portal | /open-reminder | /open-inventaris |
| Environment URL di Portal | REMINDER_APP_URL | INVENTARIS_APP_URL |
| Callback penerima | /sso/verify | /sso/verify |
| Secret | sso.jwtSecret | nilai yang sama dengan penandatangan di Portal |

Audience aplikasi baru harus unik dan sama persis di Portal dan penerima. Jangan memakai `reminder-app` untuk semua aplikasi. Secret diberikan melalui environment server, bukan ditulis di source code atau dokumentasi. Jika secret belum tersedia, buat contoh konfigurasi dan laporkan kebutuhan tersebut; jangan menghasilkan secret berbeda pada masing-masing aplikasi.

Implementasi penerima saja tidak cukup: aplikasi baru memerlukan method, route, environment URL, dan tautan menu di Portal yang menerbitkan token dengan audience aplikasi tersebut. Perubahan Portal memerlukan akses ke project Portal; bila akses tidak tersedia, berikan patch/instruksi yang jelas dan nyatakan belum diterapkan.

## 2. Alur dan implementasi Portal saat ini

1. Pengguna login di Portal.
2. Pengguna membuka `/open-reminder` di Portal.
3. `Home::redirectToReminder()` memanggil `Auth::isLoggedIn()`.
4. Portal membuat JWT dari session dan menandatangani dengan `sso.jwtSecret`.
5. Browser diarahkan ke `REMINDER_APP_URL/sso/verify?token=JWT`.
6. Penerima memverifikasi token, membuat session lokal, lalu mengarahkan browser ke halaman utama tanpa token pada URL.
7. Halaman privat penerima memeriksa session lokal pada setiap request.

Acuan repository Portal:

- `app/Controllers/Home.php`: method `redirectToReminder()`.
- `app/Config/Routes.php`: GET `/open-reminder`, GET/POST `/login`, GET `/logout`.
- `app/Controllers/Auth.php`: `isLoggedIn()` memeriksa `user_id` atau memulihkan session melalui remember-me. Portal tidak mensyaratkan session `isLoggedIn`.

**Koreksi pada komentar kode awal:** `time() + 300` adalah **300 detik / 5 menit**, bukan 30 detik. Token saat ini **bukan sekali pakai**; tanpa pencatatan pemakaian, token valid dapat digunakan ulang selama belum kedaluwarsa. JWT yang sudah habis masa berlakunya tidak otomatis mengakhiri session lokal yang telah dibuat.

Kode awal Portal memiliki fallback URL HTTP. Untuk produksi konfigurasi URL penerima harus HTTPS. Dokumentasi ini tidak mengubah file aplikasi Portal.

## 3. Kontrak token dan session

Algoritma yang diizinkan hanya `HS256`. Format callback adalah GET `/sso/verify?token=<JWT>`. Jangan menerima identitas dari query parameter lain, decode base64 saja, atau memilih algoritma berdasarkan header token tanpa pembatasan.

| Claim | Kontrak | Session penerima |
| --- | --- | --- |
| iss | wajib, string `portal-app` | tidak perlu disimpan |
| aud | wajib, string audience penerima | tidak perlu disimpan |
| iat | wajib, integer waktu Unix dalam detik | tidak perlu disimpan |
| exp | wajib, integer; lebih besar dari iat; TTL maksimum 300 detik | tidak perlu disimpan |
| user_id | wajib, ID Portal berupa integer positif atau string angka positif | user_id |
| username | wajib, string tidak kosong | username |
| role | opsional, string atau null | role, default string kosong |
| nama | opsional, string atau null | nama, default string kosong |
| divisi | opsional, string atau null | divisi, default string kosong |
| nik_absen | opsional, string atau integer atau null | nik_absen, dinormalisasi string |

Penerima menetapkan `isLoggedIn = true` hanya sesudah seluruh verifikasi berhasil. Nilai `nik_absen` boleh kosong, termasuk untuk admin; jangan memaksakan aturan kelengkapan karyawan Portal pada penerima tanpa kebutuhan bisnis. Claim `jabatan`, email, password, dan `id` tidak dikirim oleh method SSO saat ini.

`user_id` adalah ID milik Portal, bukan otomatis primary key tabel users aplikasi baru. Bila aplikasi membutuhkan user lokal, gunakan kolom unik `portal_user_id` untuk pemetaan, lakukan upsert sesuai kebutuhan, dan simpan ID lokal dengan nama terpisah seperti `local_user_id`. Jangan membuat akun berdasarkan nama atau menyamakan ID dua database. Password Portal tidak perlu disalin.

Role Portal adalah identitas yang dikirim, sedangkan hak akses fitur harus ditentukan aplikasi penerima. Tolak role tak dikenal pada fitur sensitif; jangan memberi hak admin ketika role kosong. Escape data user ketika ditampilkan, misalnya `esc(session()->get('nama'))`.

## 4. Dependency dan environment

Pastikan project benar-benar aplikasi CodeIgniter 4 dan Composer dijalankan dari root aplikasi yang memiliki `spark`. Repository acuan tidak mencantumkan `firebase/php-jwt` pada composer.json yang diperiksa dan tidak memiliki folder vendor; jangan menganggap dependency telah tersedia.

Pada kedua aplikasi, bila paket belum terpasang:

```sh
composer require firebase/php-jwt
composer show firebase/php-jwt
```

Commit composer.json dan composer.lock hasil pemasangan. Pada deployment gunakan `composer install` berdasarkan lockfile; pastikan versi paket cocok dengan PHP server dan menyediakan `Firebase\JWT\JWT` serta `Firebase\JWT\Key`.

Contoh `.env` Portal (placeholder harus diganti, jangan dipakai apa adanya):

```ini
sso.jwtSecret = 'GANTI_DENGAN_SECRET_SERVER_YANG_KUAT'
REMINDER_APP_URL = 'https://reminder.sentralmedika.co.id'
INVENTARIS_APP_URL = 'https://inventaris.sentralmedika.co.id'
```

Contoh `.env` penerima Inventaris:

```ini
CI_ENVIRONMENT = production
app.baseURL = 'https://inventaris.sentralmedika.co.id/'
sso.jwtSecret = 'GANTI_DENGAN_SECRET_YANG_SAMA_DENGAN_PORTAL'
sso.audience = 'inventaris-app'
PORTAL_URL = 'https://portal.sentralmedika.co.id'
```

Untuk Reminder, ubah audience menjadi `reminder-app` dan baseURL ke URL Reminder. `sso.audience` merupakan konfigurasi penerima pada template ini; Portal lama memakai audience hardcoded.

Gunakan secret acak dengan entropi minimal 256 bit. Contoh pembuatan sekali di lingkungan tepercaya: `php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"`. Simpan hasil di pengelola secret/environment, distribusikan secara aman, dan jangan commit `.env`. Jangan mengganti secret Portal yang sedang dipakai tanpa koordinasi seluruh penerima.

## 5. Penambahan aplikasi baru pada Portal

Contoh berikut ditambahkan ke class `App\Controllers\Home` yang sudah ada. Import `Firebase\JWT\JWT` sudah tersedia pada Home Portal. Contoh sengaja tidak menerima URL tujuan dari input pengguna.

```php
public function redirectToInventaris()
{
    $auth = new \App\Controllers\Auth();
    if (!$auth->isLoggedIn()) {
        return redirect()->to(base_url('login'));
    }

    $secret = (string) env('sso.jwtSecret', '');
    $appUrl = rtrim((string) env('INVENTARIS_APP_URL', ''), '/');
    if ($secret === '' || filter_var($appUrl, FILTER_VALIDATE_URL) === false
        || strtolower((string) parse_url($appUrl, PHP_URL_SCHEME)) !== 'https') {
        log_message('error', 'Konfigurasi SSO Inventaris tidak valid.');
        return $this->response->setStatusCode(503)->setBody('SSO belum dikonfigurasi.');
    }

    $now = time();
    $payload = [
        'iss' => 'portal-app',
        'aud' => 'inventaris-app',
        'iat' => $now,
        'exp' => $now + 300, // 5 menit; belum sekali pakai
        'user_id' => session()->get('user_id'),
        'username' => session()->get('username'),
        'role' => session()->get('role'),
        'nama' => session()->get('nama'),
        'divisi' => session()->get('divisi'),
        'nik_absen' => session()->get('nik_absen'),
    ];

    try {
        $token = JWT::encode($payload, $secret, 'HS256');
        $query = http_build_query(['token' => $token], '', '&', PHP_QUERY_RFC3986);
        return redirect()->to($appUrl . '/sso/verify?' . $query)
            ->setHeader('Cache-Control', 'no-store')
            ->setHeader('Referrer-Policy', 'no-referrer');
    } catch (\Throwable $e) {
        log_message('error', 'Penerbitan token SSO Inventaris gagal.');
        return $this->response->setStatusCode(503)->setBody('SSO tidak tersedia.');
    }
}
```

Tambahkan route tanpa menghapus route lain:

```php
$routes->get('open-inventaris', 'Home::redirectToInventaris');
```

Tautan menu Portal harus menuju route Portal, bukan langsung ke callback penerima:

```php
<a href="<?= esc(site_url('open-inventaris'), 'attr') ?>">Buka Inventaris</a>
```

Saat belum login, Portal saat ini mengarahkan ke `/login`, lalu setelah login ke halaman utama Portal. Pengguna perlu memilih aplikasi lagi. Jangan mengasumsikan tersedia fitur return URL otomatis.

## 6. Controller penerima siap adaptasi

Buat `app/Controllers/SsoController.php`. Template kompatibel dengan payload Portal saat ini dan tidak mewajibkan `jti` yang belum dikirim Portal.

```php
<?php

namespace App\Controllers;

use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use RuntimeException;
use Throwable;

class SsoController extends BaseController
{
    public function verify()
    {
        $secret = (string) env('sso.jwtSecret', '');
        $audience = trim((string) env('sso.audience', ''));
        if ($secret === '' || $audience === '') {
            log_message('error', 'SSO: secret atau audience belum dikonfigurasi.');
            return $this->response->setStatusCode(503)
                ->setHeader('Cache-Control', 'no-store')
                ->setHeader('Referrer-Policy', 'no-referrer')
                ->setBody('SSO belum dikonfigurasi. Hubungi administrator.');
        }

        $rawToken = $this->request->getGet('token');
        $token = is_string($rawToken) ? trim($rawToken) : '';
        if ($token === '' || strlen($token) > 16384) {
            return $this->rejected();
        }

        try {
            $decoded = JWT::decode($token, new Key($secret, 'HS256'));
            if (($decoded->iss ?? null) !== 'portal-app'
                || ($decoded->aud ?? null) !== $audience) {
                throw new RuntimeException('Issuer/audience tidak sesuai.');
            }
            if (!isset($decoded->iat, $decoded->exp)
                || !is_int($decoded->iat) || !is_int($decoded->exp)
                || $decoded->iat > time() || $decoded->exp <= time()
                || $decoded->exp <= $decoded->iat
                || $decoded->exp - $decoded->iat > 300) {
                throw new RuntimeException('Waktu token tidak valid.');
            }

            $userId = $decoded->user_id ?? null;
            if ((!is_int($userId) && !is_string($userId))
                || !preg_match('/^[1-9][0-9]*$/D', (string) $userId)
                || !is_string($decoded->username ?? null)
                || trim($decoded->username) === '') {
                throw new RuntimeException('Identitas token tidak lengkap.');
            }
            foreach (['role', 'nama', 'divisi'] as $field) {
                if (isset($decoded->{$field}) && !is_string($decoded->{$field})) {
                    throw new RuntimeException('Tipe profil tidak valid.');
                }
            }
            if (isset($decoded->nik_absen) && !is_string($decoded->nik_absen)
                && !is_int($decoded->nik_absen)) {
                throw new RuntimeException('Tipe NIK tidak valid.');
            }

            // Tambahkan pemetaan user lokal di sini bila memang diperlukan.
            // Semua validasi harus berhasil sebelum session autentikasi dibuat.
            $session = session();
            // Sesuaikan pembersihan ini jika project memiliki namespace auth sendiri.
            $session->remove([
                'user_id', 'username', 'nama', 'divisi', 'nik_absen', 'role',
                'isLoggedIn', 'local_user_id', 'permissions',
            ]);
            $session->regenerate(true);
            $session->set([
                'user_id' => $userId,
                'username' => $decoded->username,
                'nama' => $decoded->nama ?? '',
                'divisi' => $decoded->divisi ?? '',
                'nik_absen' => (string) ($decoded->nik_absen ?? ''),
                'role' => $decoded->role ?? '',
                'isLoggedIn' => true,
            ]);

            return redirect()->to(site_url('/'))
                ->setHeader('Cache-Control', 'no-store')
                ->setHeader('Referrer-Policy', 'no-referrer');
        } catch (Throwable $e) {
            // Jangan mencatat JWT, secret, query URL, atau isi profil pengguna.
            log_message('warning', 'Token SSO ditolak atau proses login SSO gagal.');
            return $this->rejected();
        }
    }

    private function rejected()
    {
        // Pesan ini tetap lokal; flashdata tidak melintasi aplikasi/domain.
        $portalUrl = rtrim((string) env('PORTAL_URL', 'https://portal.sentralmedika.co.id'), '/');
        return redirect()->to($portalUrl . '/login')
            ->setHeader('Cache-Control', 'no-store')
            ->setHeader('Referrer-Policy', 'no-referrer');
    }

    public function logout()
    {
        session()->destroy();
        $portalUrl = rtrim((string) env('PORTAL_URL', 'https://portal.sentralmedika.co.id'), '/');
        return redirect()->to($portalUrl . '/logout')
            ->setHeader('Cache-Control', 'no-store');
    }
}
```

Template memakai ID Portal berupa angka positif sesuai pola login repository. Bila sumber identitas kelak menggunakan UUID, ubah kontrak dan validator secara konsisten, jangan menghapus seluruh validasi. Sinkronkan jam server dengan NTP; jangan memperpanjang TTL hanya untuk menutupi kesalahan waktu.

`PORTAL_URL` harus URL server tepercaya yang dikonfigurasi administrator, memakai HTTPS di produksi. Jangan mengambilnya dari parameter request. Pada aplikasi lama yang menyimpan hak akses dengan nama lain, bersihkan pula state hak akses tersebut ketika identitas diganti, dan hitung ulang izin pengguna baru.

## 7. Filter autentikasi dan routes penerima

Buat `app/Filters/SsoAuthFilter.php`:

```php
<?php

namespace App\Filters;

use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;

class SsoAuthFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        if (session()->get('isLoggedIn') !== true || !session()->get('user_id')) {
            $portalUrl = rtrim((string) env('PORTAL_URL', 'https://portal.sentralmedika.co.id'), '/');
            return redirect()->to($portalUrl . '/login');
        }
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
    }
}
```

Tambahkan entry pada `$aliases` di `app/Config/Filters.php`, jangan menimpa alias/filter lain:

```php
'ssoauth' => \App\Filters\SsoAuthFilter::class,
```

Contoh `app/Config/Routes.php` penerima:

```php
$routes->setAutoRoute(false);

// Callback harus dapat dijangkau tanpa session autentikasi lokal.
$routes->get('sso/verify', 'SsoController::verify');
// Logout lokal diawali POST agar dapat dilindungi CSRF.
$routes->post('logout', 'SsoController::logout', ['filter' => 'csrf']);

$routes->group('', ['filter' => 'ssoauth'], static function ($routes) {
    $routes->get('/', 'Home::index');
    // Daftarkan semua route halaman dan aksi privat di sini.
});
```

Route `/` contoh harus menggantikan route `/` lama yang belum dilindungi, bukan didaftarkan ganda. Pastikan controller dan action tujuan benar-benar tersedia. Jika ada filter login global, kecualikan `sso/verify` dari filter login tersebut; jangan menonaktifkan seluruh proteksi. Pertahankan CSRF pada operasi perubahan data. Untuk API privat, adaptasi filter agar memberi HTTP 401 JSON bila belum login, dan tetap periksa izin fitur setelah autentikasi.

Contoh tombol logout penerima pada view CI4:

```php
<form action="<?= esc(site_url('logout'), 'attr') ?>" method="post">
    <?= csrf_field() ?>
    <button type="submit">Keluar</button>
</form>
```

Logout ini menghapus session penerima lalu menuju GET `/logout` Portal yang sudah ada. Logout Portal tidak otomatis menghapus session aplikasi penerima lain. Jangan mengklaim single logout seluruh aplikasi; diperlukan mekanisme tambahan untuk pencabutan sesi lintas aplikasi. Route logout Portal saat ini sendiri masih GET dan dapat ditingkatkan terpisah.

## 8. Session, cookie, dan deployment

- Session aplikasi penerima disimpan lokal/server side. Gunakan nama cookie session unik tiap aplikasi, contohnya `inventaris_session`.
- Konfigurasikan cookie host-only, HttpOnly, Secure pada HTTPS produksi, dan SameSite Lax untuk alur redirect top-level GET. Jangan berbagi cookie session atau storage session dengan Portal.
- Periksa versi CI4: nama session cookie umumnya berada di `app/Config/Session.php`, opsi cookie berada di `app/Config/Cookie.php`; versi lama dapat menaruh sebagian opsi pada App.php. Pakai properti yang benar-benar tersedia pada project, bukan menyalin nama environment yang belum didukung.
- Pastikan driver session dan savePath dapat ditulis, baseURL benar, DocumentRoot menuju public, dan rewrite/indexPage sesuai hosting. Pada reverse proxy, konfigurasikan pengenalan HTTPS dan proxy tepercaya sesuai framework.
- Callback ini memakai navigasi browser, sehingga tidak membutuhkan CORS untuk login. Jangan menambahkan wildcard CORS sebagai perbaikan SSO.
- Token bertanda tangan tidak terenkripsi: payload dapat dibaca siapa pun yang memperoleh token. Gunakan HTTPS, jangan menyertakan password atau data sensitif tambahan, dan jangan meletakkan secret/token pada JavaScript atau localStorage.
- Header no-store dan no-referrer membantu, tetapi token URL masih dapat muncul di browser history, access log, proxy, atau monitoring. Atur redaksi parameter token pada infrastruktur dan hindari resource/analytics pihak ketiga pada callback. Redirect segera ke URL bersih.
- HS256 memakai secret simetris: aplikasi pemegang secret juga mampu menandatangani token. Penerima harus tepercaya. Secret per aplikasi atau tanda tangan asimetris dapat menjadi peningkatan desain terpisah yang memerlukan perubahan Portal.

## 9. Jika membutuhkan token benar-benar sekali pakai

Template dasar tetap sesuai Portal saat ini dan belum mencegah replay. Untuk persyaratan sekali pakai:

1. Portal menambahkan `jti` acak, misalnya `bin2hex(random_bytes(16))`.
2. Penerima memvalidasi signature, issuer, audience, waktu, identitas, dan format jti.
3. Penerima mengklaim jti secara atomik pada storage bersama seluruh instance, sebelum membuat session. Contoh desain Redis: SET key value NX EX ttl; alternatif database dengan unique constraint dan transaksi. TTL mencakup sisa umur token dan toleransi waktu bila ada.
4. Bila jti telah dipakai atau storage gagal, tolak login. Bersihkan entri setelah masa token habis.
5. Jangan memakai pola cache get lalu save, karena dua request dapat lolos bersamaan. Jangan mencatat jti hanya pada session pengguna atau memory proses.

Aktifkan kewajiban jti pada penerima setelah Portal mengirimkannya. Untuk rancangan yang lebih kuat, gunakan authorization code sekali pakai dan pertukaran server-to-server; itu perubahan protokol, bukan kemampuan kode saat ini. Untuk mengikat login ke browser peminta, tambahkan state/nonce yang tervalidasi melalui alur yang disepakati kedua aplikasi; template saat ini belum menyediakan binding tersebut.

## 10. Pengujian sebelum dinyatakan selesai

Jalankan pengecekan dari root aplikasi yang sesuai:

```sh
php -l app/Controllers/SsoController.php
php -l app/Filters/SsoAuthFilter.php
php spark routes
composer show firebase/php-jwt
```

Lint hanya memeriksa sintaks. Verifikasi integrasi tetap diperlukan:

| Skenario | Hasil yang diwajibkan |
| --- | --- |
| Login Portal lalu pilih aplikasi | callback berhasil, URL bersih, session lokal lengkap |
| Buka halaman privat tanpa session | menuju login Portal; API memberi 401 jika memakai filter API |
| Callback tanpa token atau token berbentuk array | ditolak tanpa error PHP |
| Token rusak, signature salah, atau secret berbeda | ditolak, tidak membuat session login baru |
| Issuer/audience salah | ditolak termasuk token Reminder yang dikirim ke Inventaris |
| Token exp hilang, kedaluwarsa, iat masa depan, atau TTL > 300 | ditolak |
| User ID/username hilang atau tipe profil salah | ditolak |
| NIK/profil opsional kosong | diterima; izin fitur tetap diperiksa |
| Secret/audience environment kosong | HTTP 503, tanpa membocorkan secret |
| Refresh halaman utama setelah SSO | session tetap tersedia tanpa token URL |
| Logout melalui form POST | session penerima terhapus, menuju logout Portal |
| Pergantian pengguna pada browser yang sama | tidak mewarisi izin/cache identitas sebelumnya |
| Replay token valid sebelum exp | masih diterima pada mode dasar; ditolak hanya bila mekanisme sekali pakai diterapkan |

Gunakan token pengujian dari environment uji, bukan secret/token produksi yang ditempel di chat, log, atau repository. Uji penolakan token dengan browser/session baru agar session lama tidak menutupi hasil. Catat hasil aktual dan hal yang belum dapat diuji; jangan menyatakan bebas error berdasarkan lint saja.

## 11. Troubleshooting

| Gejala | Pemeriksaan/perbaikan |
| --- | --- |
| Class Firebase JWT/Key not found | paket terpasang pada root yang benar, versi mendukung Key, vendor tersedia dan autoload aktif |
| Signature verification failed | nilai secret persis sama, termasuk spasi, tanda kutip, dan environment server yang aktif |
| Expired/before valid token | jam kedua server, waktu Unix detik, jaringan; ambil token baru lewat Portal |
| Issuer/audience ditolak | iss portal-app dan audience unik sama persis pada kedua pihak |
| /sso/verify 404 | route GET, namespace/controller, rewrite, baseURL dan subdirectory |
| Redirect berulang | callback terkena filter login, session gagal tersimpan, atau route privat memeriksa nama session berbeda |
| Login sukses tetapi kembali dianggap logout | cookie unik/domain/path/Secure, HTTPS proxy, savePath dan driver session |
| Data user tidak lengkap | login ulang Portal; cocokkan nama claim, jangan mengandalkan jabatan/email yang tidak dikirim |
| Pesan error penerima tidak terlihat di Portal | flashdata lokal tidak berpindah domain; tampilkan halaman error lokal bila dibutuhkan |
| Token aplikasi baru ditolak Reminder | normal: audience berbeda, gunakan callback aplikasi yang benar |
| Logout Portal tetapi app lain masih login | session lokal independen; single logout belum tersedia |


## 12. Referensi teknis

- [PHP-JWT: instalasi, encode dan decode dengan algoritma yang dibatasi](https://github.com/firebase/php-jwt).
- [CodeIgniter 4: controller filters dan route protection](https://codeigniter4.github.io/userguide/incoming/filters.html).
- [CodeIgniter 4: session library dan regenerasi session](https://codeigniter4.github.io/userguide/libraries/sessions.html).

Sesuaikan API dengan versi dependency yang benar-benar terpasang. Dokumen ini adalah panduan implementasi dan pengujian; contoh belum dieksekusi pada aplikasi penerima yang tidak tersedia dalam workspace.
